Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/workato_platform/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ def cli(
# Core setup and configuration commands
cli.add_command(init.init)
cli.add_command(projects.projects)
cli.add_command(projects.projects, name="project")
cli.add_command(profiles.profiles)
cli.add_command(profiles.profiles, name="profiles")
cli.add_command(properties.properties)
cli.add_command(properties.properties, name="property")

# Development commands
cli.add_command(guide.guide)
Expand All @@ -77,12 +80,19 @@ def cli(

# API and resource management commands
cli.add_command(api_collections.api_collections)
cli.add_command(api_collections.api_collections, name="api-collection")
cli.add_command(api_clients.api_clients)
cli.add_command(api_clients.api_clients, name="api-client")
cli.add_command(data_tables.data_tables)
cli.add_command(data_tables.data_tables, name="data-table")
cli.add_command(connections.connections)
cli.add_command(connections.connections, name="connection")
cli.add_command(connectors.connectors)
cli.add_command(connectors.connectors, name="connector")
cli.add_command(recipes.recipes)
cli.add_command(recipes.recipes, name="recipe")

# Information commands
cli.add_command(assets.assets)
cli.add_command(assets.assets, name="asset")
cli.add_command(workspace.workspace)
159 changes: 138 additions & 21 deletions src/workato_platform/cli/commands/init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"""Initialize Workato CLI for a new project"""

import json

from typing import Any

import asyncclick as click
import certifi

from workato_platform import Workato
from workato_platform.cli.commands.projects.project_manager import ProjectManager
Expand Down Expand Up @@ -28,6 +33,12 @@
is_flag=True,
help="Run in non-interactive mode (requires all necessary options)",
)
@click.option(
"--output-mode",
type=click.Choice(["table", "json"]),
default="table",
help="Output format: table (default) or json (only with --non-interactive)",
)
@handle_api_exceptions
async def init(
profile: str | None = None,
Expand All @@ -37,32 +48,71 @@ async def init(
project_name: str | None = None,
project_id: int | None = None,
non_interactive: bool = False,
output_mode: str = "table",
) -> None:
"""Initialize Workato CLI for a new project"""

# Validate that --output-mode json requires --non-interactive
if output_mode == "json" and not non_interactive:
# Return JSON error for consistency
error_data: dict[str, Any] = {
"status": "error",
"error": "--output-mode json can only be used with --non-interactive flag",
"error_code": "INVALID_OPTIONS",
}
click.echo(json.dumps(error_data))
return

if non_interactive:
# Validate required parameters for non-interactive mode
error_msg = None
error_code = None

# Either profile OR individual attributes (region, api_token) are required
if not profile and not (region and api_token):
click.echo(
"Either --profile or both --region and --api-token are "
"required in non-interactive mode"
error_msg = (
"Either --profile or both --region and --api-token are required "
"in non-interactive mode"
)
raise click.Abort()
if region == "custom" and not api_url:
click.echo(
"--api-url is required when region=custom in non-interactive mode"
error_code = "MISSING_REQUIRED_OPTIONS"
elif region == "custom" and not api_url:
error_msg = (
"--api-url is required when region=custom in non-interactive mode"
)
raise click.Abort()
if not project_name and not project_id:
click.echo(
"Either --project-name or --project-id is "
"required in non-interactive mode"
error_code = "MISSING_REQUIRED_OPTIONS"
elif not project_name and not project_id:
error_msg = (
"Either --project-name or --project-id is required in "
"non-interactive mode"
)
raise click.Abort()
if project_name and project_id:
click.echo("❌ Cannot specify both --project-name and --project-id")
raise click.Abort()
error_code = "MISSING_REQUIRED_OPTIONS"
elif project_name and project_id:
error_msg = "Cannot specify both --project-name and --project-id"
error_code = "CONFLICTING_OPTIONS"

if error_msg:
if output_mode == "json":
error_data = {
"status": "error",
"error": error_msg,
"error_code": error_code,
}
click.echo(json.dumps(error_data))
return
else:
click.echo(f"❌ {error_msg}")
raise click.Abort() # For non-JSON mode, keep the normal abort behavior

# Initialize JSON output data structure if in JSON mode
# Since we've already validated that json mode requires non-interactive,
# we can use output_data existence as our flag throughout the code
output_data = None
if output_mode == "json":
output_data = {
"status": "success",
"profile": {},
"project": {},
}

config_manager = await ConfigManager.initialize(
profile_name=profile,
Expand All @@ -71,10 +121,44 @@ async def init(
api_url=api_url,
project_name=project_name,
project_id=project_id,
output_mode=output_mode,
non_interactive=non_interactive,
)

# Check if project directory exists and is non-empty
project_dir = config_manager.get_project_directory()
if project_dir and project_dir.exists() and any(project_dir.iterdir()):
# Directory is non-empty
if non_interactive:
# In non-interactive mode, fail with error
error_msg = (
f"Directory '{project_dir}' is not empty. "
"Please use an empty directory or remove existing files."
)
if output_mode == "json":
error_data = {
"status": "error",
"error": error_msg,
"error_code": "DIRECTORY_NOT_EMPTY",
}
click.echo(json.dumps(error_data))
return
else:
click.echo(f"❌ {error_msg}")
raise click.Abort()
else:
# Interactive mode - ask for confirmation
click.echo(f"⚠️ Directory '{project_dir}' is not empty.")
if not click.confirm(
"Proceed with initialization? This may overwrite or delete files.",
default=False,
):
click.echo("❌ Initialization cancelled")
return

# Automatically run pull to set up project structure
click.echo()
if not output_data:
click.echo()

# Get API credentials from the newly configured profile
config_data = config_manager.load_config()
Expand All @@ -83,17 +167,50 @@ async def init(
project_profile_override
)

# Populate profile data for JSON output
if output_data:
profile_data = config_manager.profile_manager.get_profile(
project_profile_override
or config_manager.profile_manager.get_current_profile_name()
or "",
)
if profile_data:
output_data["profile"] = {
"name": project_profile_override
or config_manager.profile_manager.get_current_profile_name(),
"region": profile_data.region,
"region_name": profile_data.region_name,
"api_url": profile_data.region_url,
"workspace_id": profile_data.workspace_id,
}

# Create API client configuration
api_config = Configuration(access_token=api_token, host=api_host)
api_config.verify_ssl = False
api_config = Configuration(
access_token=api_token, host=api_host, ssl_ca_cert=certifi.where()
)

# Create project manager and run pull
async with Workato(configuration=api_config) as workato_api_client:
project_manager = ProjectManager(workato_api_client=workato_api_client)
await _pull_project(
config_manager=config_manager,
project_manager=project_manager,
non_interactive=non_interactive,
)

# Final completion message
click.echo("🎉 Project setup complete!")
# Populate project data for JSON output
if output_data:
meta_data = config_manager.load_config()
project_path = config_manager.get_project_directory()
output_data["project"] = {
"name": meta_data.project_name or "project",
"id": meta_data.project_id,
"folder_id": meta_data.folder_id,
"path": str(project_path) if project_path else None,
}

# Output final result
if output_data:
click.echo(json.dumps(output_data))
else:
click.echo("🎉 Project setup complete!")
109 changes: 106 additions & 3 deletions src/workato_platform/cli/commands/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async def use(
config_data = config_manager.load_config()
except Exception:
workspace_root = None
config_data = ConfigData()
config_data = ConfigData.model_construct()

# If we have a workspace config (project_id exists), update workspace profile
if config_data.project_id and workspace_root:
Expand All @@ -162,8 +162,15 @@ async def use(


@profiles.command()
@click.option(
"--output-mode",
type=click.Choice(["table", "json"]),
default="table",
help="Output format: table (default) or json",
)
@inject
async def status(
output_mode: str = "table",
config_manager: ConfigManager = Provide[Container.config_manager],
) -> None:
"""Show current profile status and configuration"""
Expand All @@ -176,11 +183,107 @@ async def status(
project_profile_override
)

output_data: dict[str, Any] = {}

if not current_profile_name:
click.echo("❌ No active profile configured")
click.echo("💡 Run 'workato init' to create and set a profile")
if output_mode == "json":
output_data = {"profile": None, "error": "No active profile configured"}
click.echo(json.dumps(output_data, indent=2))
else:
click.echo("❌ No active profile configured")
click.echo("💡 Run 'workato init' to create and set a profile")
return

# JSON output mode
if output_mode == "json":
# Profile information
profile_source_type = None
profile_source_location = None
if project_profile_override:
profile_source_type = "project_override"
profile_source_location = ".workatoenv"
elif os.environ.get("WORKATO_PROFILE"):
profile_source_type = "environment_variable"
profile_source_location = "WORKATO_PROFILE"
else:
profile_source_type = "global_default"
profile_source_location = "~/.workato/profiles"

profile_data = config_manager.profile_manager.get_current_profile_data(
project_profile_override
)

output_data["profile"] = {
"name": current_profile_name,
"source": {
"type": profile_source_type,
"location": profile_source_location,
},
}

if profile_data:
output_data["profile"]["configuration"] = {
"region": {
"code": profile_data.region,
"name": profile_data.region_name,
"url": profile_data.region_url,
},
"workspace_id": profile_data.workspace_id,
}

# Authentication information
api_token, _ = config_manager.profile_manager.resolve_environment_variables(
project_profile_override
)

if api_token:
auth_source_type = None
auth_source_location = None
if os.environ.get("WORKATO_API_TOKEN"):
auth_source_type = "environment_variable"
auth_source_location = "WORKATO_API_TOKEN"
else:
auth_source_type = "keyring"
auth_source_location = "~/.workato/profiles"

output_data["authentication"] = {
"configured": True,
"source": {"type": auth_source_type, "location": auth_source_location},
}
else:
output_data["authentication"] = {"configured": False}

# Project information
try:
workspace_root = config_manager.get_workspace_root()
if workspace_root and (config_data.project_id or config_data.project_name):
project_metadata: dict[str, Any] = {}
if config_data.project_name:
project_metadata["name"] = config_data.project_name
if config_data.project_id:
project_metadata["id"] = config_data.project_id
if config_data.folder_id:
project_metadata["folder_id"] = config_data.folder_id

project_path = config_manager.get_project_directory()

if project_path:
output_data["project"] = {
"configured": True,
"path": str(project_path),
"metadata": project_metadata,
}
else:
output_data["project"] = {"configured": False}
else:
output_data["project"] = {"configured": False}
except Exception:
output_data["project"] = {"configured": False}

click.echo(json.dumps(output_data))
return

# Table output (existing code)
click.echo("📊 Profile Status:")
click.echo(f" Current Profile: {current_profile_name}")

Expand Down
Loading