Skip to content

CLI Commands API Reference

This page provides comprehensive documentation for all SFAI CLI commands, including arguments, options, and usage examples.

sfai app - Application Management

init_cmd

Initialize a new SFAI application in the current directory.

Parameters:

Name Type Description Default
force bool

bool Whether to force initialization if app is already initialized

Option(False, '--force', help='Force initialize')
template str

str Template to use for scaffolding the app (eg: fastapi_hello or flask_hello)

Option('fastapi_hello', '--template', help='Template to use (eg: fastapi_hello or flask_hello)')

Returns:

Type Description
None

None

Source code in sfai/cli/app/init.py
@app.callback(invoke_without_command=True, help="Initialize your app")
def init_cmd(
    force: bool = typer.Option(False, "--force", help="Force initialize"),
    template: str = typer.Option(
        "fastapi_hello",
        "--template",
        help="Template to use (eg: fastapi_hello or flask_hello)",
    ),
) -> None:
    """
    Initialize a new SFAI application in the current directory.

    Args:
        force: bool
            Whether to force initialization if app is already initialized
        template: str
            Template to use for scaffolding the app (eg: fastapi_hello or flask_hello)

    Returns:
        None
    """
    # Call the Python API function
    result = init(template=template, force=force)

    if not result.success:
        console.print(
            f"{WARNING_EMOJI} [{WARNING_COLOR}] App {result.app_name} "
            f"already initialized use --force to reinitialize[/]"
        )
        return

    app_name = result.app_name

    console.print(f"{SUCCESS_EMOJI} App initialized!")
    console.print(f"{APP_NAME_EMOJI} App name: {app_name}")
    console.print(f"{TEMPLATE_EMOJI} Template: {template}")

    console.print(
        f"\n{ROCKET_EMOJI} Next: Run `[cyan]sfai app deploy[/]` to build "
        f"and deploy your app.\n"
        f" Run `[cyan]sfai platform init[/]` to initialize a cloud "
        f"environment(heroku, aws).\n"
        f"{LIGHT_BULB_EMOJI} Tip: Run `[cyan]sfai app open[/]` to access "
        f"your app locally."
    )

Example:

# Initialize with default template
sfai app init

# Initialize with specific template
sfai app init --template fastapi_hello

# Force reinitialize existing app
sfai app init --force --template flask_hello


deploy_cmd

Deploy the current application to the specified environment.

Parameters:

Name Type Description Default
platform Optional[str]

Optional[str] Platform to deploy to

Option(None, help='Platform to deploy to')
environment Optional[str]

str Environment to deploy to (defaults to "default")

Option(None, help='Environment to deploy to')
path str

str Path to the app folder

Option('.', help='Path to the app folder (default: current directory)')
values_path Optional[str]

Optional[str] Path to Helm values file

Option(None, help='Path to Helm values file')
set_values Optional[str]

Optional[str] Additional values to set for Helm

Option(None, help='Additional values to set for Helm')
commit_message Optional[str]

Optional[str] Commit message

Option(None, help='Commit message')
branch Optional[str]

Optional[str] Branch name

Option(None, help='Branch name')

Returns: None

Source code in sfai/cli/app/deploy.py
@app.callback(
    invoke_without_command=True,
    help="Deploy an application to the configured environment.",
)
def deploy_cmd(
    # common options
    platform: Optional[str] = typer.Option(None, help="Platform to deploy to"),
    environment: Optional[str] = typer.Option(None, help="Environment to deploy to"),
    path: str = typer.Option(
        ".", help="Path to the app folder (default: current directory)"
    ),
    # k8s options
    values_path: Optional[str] = typer.Option(None, help="Path to Helm values file"),
    set_values: Optional[str] = typer.Option(
        None, help="Additional values to set for Helm"
    ),
    # heroku options
    commit_message: Optional[str] = typer.Option(None, help="Commit message"),
    branch: Optional[str] = typer.Option(None, help="Branch name"),
) -> None:
    """
    Deploy the current application to the specified environment.

    Args:
        platform: Optional[str]
            Platform to deploy to
        environment: str
            Environment to deploy to (defaults to "default")
        path: str
            Path to the app folder
        values_path: Optional[str]
            Path to Helm values file
        set_values: Optional[str]
            Additional values to set for Helm
        commit_message: Optional[str]
            Commit message
        branch: Optional[str]
            Branch name
    Returns:
        None
    """

    console.print(f"{ROCKET_EMOJI} Deploying application...")

    result = deploy(
        platform=platform,
        environment=environment,
        path=path,
        values_path=values_path,
        set_values=set_values,
        commit_message=commit_message,
        branch=branch,
    )

    if result.success:
        console.print(f"{CELEBRATE_EMOJI} {result.message}")
    else:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")

Examples:

# Deploy to current platform
sfai app deploy

# Deploy to specific platform
sfai app deploy --platform heroku

# Deploy with custom Helm values
sfai app deploy --values-path ./values.yaml --set-values "image.tag=v1.2.3"

# Deploy to Heroku with commit message
sfai app deploy --platform heroku --commit-message "Feature: Add new API endpoint"


status_cmd

Show the status of the current app from context.

Parameters:

Name Type Description Default
platform Optional[str]

Optional[str] Platform to show status from

Option(None, help='Platform to show status from')
environment Optional[str]

str Environment to show status from (defaults to "default")

Option(None, help='Environment to show status from')
Source code in sfai/cli/app/status.py
@app.callback(invoke_without_command=True, help="Show status of the current app")
def status_cmd(
    platform: Optional[str] = typer.Option(None, help="Platform to show status from"),
    environment: Optional[str] = typer.Option(
        None, help="Environment to show status from"
    ),
) -> None:
    """
    Show the status of the current app from context.

    Args:
        platform: Optional[str]
            Platform to show status from
        environment: str
            Environment to show status from (defaults to "default")
    """

    result = status(platform=platform, environment=environment)
    if not result.success:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")
        return

Examples:

# Show status from current platform
sfai app status

# Show status from specific platform
sfai app status --platform heroku


logs_cmd

Show logs for the current app.

Parameters:

Name Type Description Default
platform Optional[str]

Optional[str] Platform to show logs from

Option(None, help='Platform to show logs from')
environment Optional[str]

str Environment to show logs from (defaults to "default")

Option(None, help='Environment to show logs from')
Source code in sfai/cli/app/logs.py
@app.callback(invoke_without_command=True, help="Show logs for the current app")
def logs_cmd(
    platform: Optional[str] = typer.Option(None, help="Platform to show logs from"),
    environment: Optional[str] = typer.Option(
        None, help="Environment to show logs from"
    ),
) -> None:
    """
    Show logs for the current app.

    Args:
        platform: Optional[str]
            Platform to show logs from
        environment: str
            Environment to show logs from (defaults to "default")
    """

    result = logs(platform=platform, environment=environment)
    if not result.success:
        console.print(
            f"{ERROR_EMOJI} [{ERROR_COLOR}] unable to fetch logs: {result.error}[/]"
        )
        return

Examples:

# Show logs from current platform
sfai app logs

# Show logs from specific platform
sfai app logs --platform eks


delete_cmd

Delete the current app deployment

Parameters:

Name Type Description Default
platform Optional[str]

Optional[str] Platform to delete from

Option(None, help='Platform to delete from')
environment Optional[str]

str Environment to delete from (defaults to "default")

Option(None, help='Environment to delete from')
Source code in sfai/cli/app/delete.py
@app.callback(invoke_without_command=True, help="Delete the current app")
def delete_cmd(
    platform: Optional[str] = typer.Option(None, help="Platform to delete from"),
    environment: Optional[str] = typer.Option(None, help="Environment to delete from"),
):
    """
    Delete the current app deployment

    Args:
        platform: Optional[str]
            Platform to delete from
        environment: str
            Environment to delete from (defaults to "default")
    """

    result = delete(platform=platform, environment=environment)

    if not result.success:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")
        raise typer.Exit(code=1)

    name = result.app_name

    console.print(f"{SUCCESS_EMOJI} Deleted app '{name}'...")

Examples:

# Delete from current platform
sfai app delete

# Delete from specific platform
sfai app delete --platform heroku


open_cmd

Open the current app in your browser.

Parameters:

Name Type Description Default
platform Optional[str]

Optional[str] Platform to open

Option(None, help='Platform to open')
environment Optional[str]

str Environment to open (defaults to "default")

Option(None, help='Environment to open')
path str

str The path to open default: /docs

Option('/docs', help='API path to open')
port int

int The port to open default: 8080

Option(8080, help='Local port')
tunnel bool

bool Whether to use a tunnel default: False

Option(False, help='Use Cloudflare tunnel')
url str

str The URL to open

Option(None, help='URL to open')

Returns: dict[str, Any]

Source code in sfai/cli/app/open.py
@app.callback(invoke_without_command=True, help="Open the current app in your browser")
def open_cmd(
    platform: Optional[str] = typer.Option(None, help="Platform to open"),
    environment: Optional[str] = typer.Option(None, help="Environment to open"),
    path: str = typer.Option("/docs", help="API path to open"),
    port: int = typer.Option(8080, help="Local port"),
    tunnel: bool = typer.Option(False, help="Use Cloudflare tunnel"),
    url: str = typer.Option(None, help="URL to open"),
):
    """
    Open the current app in your browser.

    Args:
        platform: Optional[str]
            Platform to open
        environment: str
            Environment to open (defaults to "default")
        path: str
            The path to open
            default: /docs
        port: int
            The port to open
            default: 8080
        tunnel: bool
            Whether to use a tunnel
            default: False
        url: str
            The URL to open
    Returns:
        dict[str, Any]
    """

    result = open(
        platform=platform,
        environment=environment,
        path=path,
        port=port,
        tunnel=tunnel,
        url=url,
    )

    if not result.success:
        console.print(f"{ERROR_EMOJI} [red]{result.error}[/]")
        raise typer.Exit(1)

    if result.url:
        console.print(
            f"{PORT_EMOJI} Opening {result.platform} app: [bold blue]{result.url}[/]"
        )
        webbrowser.open(result.url)
        return

    # Port-forward confirmed
    app_name = result.app_name
    console.print(
        f"{PORT_EMOJI} Port-forwarding svc/{app_name}-service → http://localhost:{port}"
    )
    time.sleep(3)
    if result.pf_proc.poll() is not None:
        console.print(f"{ERROR_EMOJI} Port-forwarding failed. Check service status.")
        return
    console.print(f"{SUCCESS_EMOJI} Port forwarding established.")

    tunnel_proc = None
    if tunnel:
        try:
            url, tunnel_proc = start_cloudflare_tunnel(port, path)
            console.print(f"{LINK_EMOJI} Tunnel ready: [bold blue]{url}[/]")
            webbrowser.open(url)
            console.print(f"{TUNNEL_EMOJI} Press Ctrl+C to stop...")
            tunnel_proc.wait()
        finally:
            if tunnel_proc and tunnel_proc.poll() is None:
                tunnel_proc.terminate()
    elif result["platform"] != "local" and result["tunnel"]:
        console.print(f"{ERROR_EMOJI} Tunnel is only supported for local apps")
    else:
        url = f"http://localhost:{port}{path}"
        console.print(f"{WEB_EMOJI} Opening [bold blue]{url}[/]")
        webbrowser.open(url)
        result["pf_proc"].wait()

    if result["pf_proc"].poll() is None:
        result["pf_proc"].terminate()

start_cloudflare_tunnel

Start a Cloudflare tunnel for a service.

Parameters:

Name Type Description Default
port int

int The port to forward

required
path str

str The path to open

required

Returns: tuple[str, subprocess.Popen]

Source code in sfai/cli/app/open.py
def start_cloudflare_tunnel(port: int, path: str) -> tuple[str, subprocess.Popen]:
    """
    Start a Cloudflare tunnel for a service.

    Args:
        port: int
            The port to forward
        path: str
            The path to open
    Returns:
        tuple[str, subprocess.Popen]
    """
    proc = subprocess.Popen(
        ["cloudflared", "tunnel", "--url", f"http://localhost:{port}"],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1,
    )
    url_holder = {"url": None}

    def parse():
        for line in iter(proc.stdout.readline, ""):
            match = re.search(r"https://[^\s]+\.trycloudflare\.com", line)
            if match:
                candidate = match.group(0).rstrip("/") + path
                try:
                    if requests.get(candidate, timeout=5).status_code == 200:
                        url_holder["url"] = candidate
                        break
                except requests.RequestException:
                    continue

    threading.Thread(target=parse, daemon=True).start()

    with Status(f"{UPDATE_EMOJI} Finding tunnel URL...", console=console):
        for _ in range(MAX_RETRIES):
            if url_holder["url"] or proc.poll() is not None:
                break
            time.sleep(RETRY_DELAY)

    if not url_holder["url"]:
        proc.terminate()
        raise RuntimeError(f"{ERROR_EMOJI} Tunnel URL failed after retries.")

    return url_holder["url"], proc

Examples:

# Open app with default settings (opens /docs on port 8080)
sfai app open

# Open specific path
sfai app open --path /api/v1/health

# Open on custom port
sfai app open --port 3000

# Open specific platform
sfai app open --platform heroku --path /api/status


Examples:

# Show current context
sfai app context

# Delete current context
sfai app context --delete


Examples:

# Publish with interactive mode
sfai app publish --interactive

# Publish with specific parameters
sfai app publish --name my-api --version 1.0.0 --description "My API" --profile production

# Publish with tags
sfai app publish --name my-api --tags api --tags production --tags v1

# Publish with OpenAPI spec
sfai app publish --name my-api --oas-file ./openapi.yaml --endpoint-uri https://api.example.com


sfai config - Service Configuration

init_cmd

Configure service connection settings.

Parameters:

Name Type Description Default
service str

str Name of the service to configure credentials for

Option(None, help='service name, e.g. mulesoft, heroku, aws')
profile_name str

str Name of the profile to store credentials under

Option(None, help='profile name to store credentials under')
interactive bool

bool Run in interactive mode

Option(False, '-i', '--interactive', help='Run in interactive mode')
org_id str

str MuleSoft organization ID

Option(None, help='MuleSoft organization ID')
environment_id str

str MuleSoft environment ID

Option(None, help='MuleSoft environment ID')
client_id str

str MuleSoft client ID

Option(None, help='MuleSoft client ID')
client_secret str

str MuleSoft client secret

Option(None, help='MuleSoft client secret')

Returns: None

Source code in sfai/cli/config/init.py
@app.callback(invoke_without_command=True)
def init_cmd(
    service: str = typer.Option(None, help="service name, e.g. mulesoft, heroku, aws"),
    profile_name: str = typer.Option(
        None, help="profile name to store credentials under"
    ),
    interactive: bool = typer.Option(
        False, "-i", "--interactive", help="Run in interactive mode"
    ),
    # credentials flags
    org_id: str = typer.Option(None, help="MuleSoft organization ID"),
    environment_id: str = typer.Option(None, help="MuleSoft environment ID"),
    client_id: str = typer.Option(None, help="MuleSoft client ID"),
    client_secret: str = typer.Option(None, help="MuleSoft client secret"),
) -> None:
    """
    Configure service connection settings.

    Args:
        service: str
            Name of the service to configure credentials for
        profile_name: str
            Name of the profile to store credentials under
        interactive: bool
            Run in interactive mode
        org_id: str
            MuleSoft organization ID
        environment_id: str
            MuleSoft environment ID
        client_id: str
            MuleSoft client ID
        client_secret: str
            MuleSoft client secret
    Returns:
        None
    """
    # Prompt for service if missing
    if not service:
        service = Prompt.ask(
            "Which service to configure (e.g., mulesoft)", default="mulesoft"
        )

    # Get schema fields and validate service
    schema_fields = _get_schema_fields(service)
    if not schema_fields:
        console.print(
            f"{ERROR_EMOJI} [{ERROR_COLOR}]Invalid or missing service: {service}[/]"
        )
        return

    # Define profile name
    profile_name = profile_name or "default"
    if interactive:
        profile_name = Prompt.ask(
            f"Enter {service} profile name for storing credentials", default="default"
        )

    # all CLI values captured
    all_cli_args = {
        "org_id": org_id,
        "environment_id": environment_id,
        "client_id": client_id,
        "client_secret": client_secret,
    }

    credentials = {}

    # Collect credentials
    if interactive:
        for field_name, description in schema_fields.items():
            val = Prompt.ask(
                f"{SECURITY_EMOJI} {description}",
                password=("secret" in field_name.lower()),
            )
            if not val:
                console.print(
                    f"{ERROR_EMOJI} [{ERROR_COLOR}]Missing required field: "
                    f"{description}[/]"
                )
                return
            credentials[field_name] = val
    else:
        for field_name in schema_fields:
            val = all_cli_args.get(field_name)
            if val is not None:
                credentials[field_name] = val

        if not credentials:
            for field_name, description in schema_fields.items():
                val = Prompt.ask(
                    f"{SECURITY_EMOJI} {description}",
                    password=("secret" in field_name.lower()),
                )
                if not val:
                    console.print(
                        f"{ERROR_EMOJI} [{ERROR_COLOR}]Missing required field: "
                        f"{description}[/]"
                    )
                    return
                credentials[field_name] = val

        # warn if irrelevant keys were passed
        invalid_keys = [
            k
            for k in all_cli_args
            if k not in schema_fields and all_cli_args[k] is not None
        ]
        if invalid_keys:
            console.print(
                f"{WARNING_EMOJI} Ignored keys not part of {service}: "
                f"{', '.join(invalid_keys)}"
            )

    # Use the API function
    result = init(service=service, config=credentials, profile_name=profile_name)

    if result.success:
        console.print(
            f"{SUCCESS_EMOJI} [{SUCCESS_COLOR}]{service.capitalize()}[/] "
            f"credentials saved under profile [bold]{profile_name}[/]."
        )
    else:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")

Examples:

# Interactive configuration
sfai config init --service mulesoft --interactive

# Direct configuration
sfai config init --service mulesoft --profile-name production \
  --org-id abc123 --environment-id def456 --client-id ghi789 --client-secret secret123

# Use default profile
sfai config init --service mulesoft --org-id abc123 --client-id ghi789 --client-secret secret123


update_cmd

Update specific values in an existing service profile

Parameters:

Name Type Description Default
service str

str Service name

Option(..., help='Service name')
profile_name Optional[str]

Optional[str] Profile to update

Option('default', help='Profile to update')
interactive bool

bool Run in interactive mode

Option(False, '--interactive', '-i')
org_id Optional[str]

Optional[str] MuleSoft organization ID

None
environment_id Optional[str]

Optional[str] MuleSoft environment ID

None
client_id Optional[str]

Optional[str] MuleSoft client ID

None
client_secret Optional[str]

Optional[str] MuleSoft client secret

None
Source code in sfai/cli/config/update.py
@app.callback(invoke_without_command=True)
def update_cmd(
    service: str = typer.Option(..., help="Service name"),
    profile_name: Optional[str] = typer.Option("default", help="Profile to update"),
    interactive: bool = typer.Option(False, "--interactive", "-i"),
    # full list of possible fields
    org_id: Optional[str] = None,
    environment_id: Optional[str] = None,
    client_id: Optional[str] = None,
    client_secret: Optional[str] = None,
):
    """Update specific values in an existing service profile

    Args:
        service: str
            Service name
        profile_name: Optional[str]
            Profile to update
        interactive: bool
            Run in interactive mode
        org_id: Optional[str]
            MuleSoft organization ID
        environment_id: Optional[str]
            MuleSoft environment ID
        client_id: Optional[str]
            MuleSoft client ID
        client_secret: Optional[str]
            MuleSoft client secret
    """
    schema = _get_service_schema(service)
    if not schema:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]Invalid service: {service}[/]")
        return

    # Check if profile exists BEFORE prompting for updates
    exists, error = profile_exists(service, profile_name)
    if not exists:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{error}[/]")
        return

    schema_keys = set(schema.keys())

    all_cli_args = {
        "org_id": org_id,
        "environment_id": environment_id,
        "client_id": client_id,
        "client_secret": client_secret,
    }

    updates = {}

    if interactive or not any(all_cli_args.get(key) for key in schema_keys):
        # prompt user for each field
        for key, label in schema.items():
            val = Prompt.ask(
                f"{UPDATE_EMOJI} New value for {label} (leave blank to skip)"
            )
            if val:
                updates[key] = val
    else:
        # use only explicitly provided cli args
        for key in schema_keys:
            val = all_cli_args.get(key)
            if val:
                updates[key] = val

    # Don't update if no changes
    if not updates:
        console.print(f"{SUCCESS_EMOJI} No updates provided. Profile unchanged.")
        return

    # Call the API
    result = update(service, updates, profile_name)

    if result.success:
        console.print(
            f"{SUCCESS_EMOJI} Updated [{SUCCESS_COLOR}]{service}[/] profile "
            f"[bold]{profile_name}[/] with new values."
        )
    else:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")

Examples:

# Update specific fields
sfai config update --service mulesoft --profile-name production --client-secret new-secret

# Interactive update
sfai config update --service mulesoft --interactive

# Update multiple fields
sfai config update --service mulesoft --org-id new-org --environment-id new-env


list_cmd

List service profiles

Parameters:

Name Type Description Default
service Optional[str]

Optional[str] Filter by specific service name

Option(None, help='Filter by specific service name')
Source code in sfai/cli/config/list.py
@app.callback(invoke_without_command=True)
def list_cmd(
    service: Optional[str] = typer.Option(None, help="Filter by specific service name"),
):
    """List service profiles

    Args:
        service: Optional[str]
            Filter by specific service name
    """
    result = list(service)

    if not result.success:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")
        return

    profiles = result.profiles

    if not profiles:
        console.print(f"[{WARNING_COLOR}]No service profiles found.[/]")
        return

    if service:
        # Single service display
        console.print(f"[{SUCCESS_COLOR}]{service.capitalize()} Profiles[/]:")
        for name in profiles:
            console.print(f"- {name}")
    else:
        # All services display
        for svc, svc_profiles in profiles.items():
            console.print(f"\n[{SUCCESS_COLOR}]{svc.capitalize()} Profiles[/]:")
            for name in svc_profiles:
                console.print(f"- {name}")

Examples:

# List all profiles
sfai config list

# List profiles for specific service
sfai config list --service mulesoft


view_cmd

View details of a service profile

Parameters:

Name Type Description Default
service str

str Service name

Option(..., help='Service name')
profile_name Optional[str]

Optional[str] Profile to view

Option('default', help='Profile to view')
Source code in sfai/cli/config/view.py
@app.callback(invoke_without_command=True)
def view_cmd(
    service: str = typer.Option(..., help="Service name"),
    profile_name: Optional[str] = typer.Option("default", help="Profile to view"),
):
    """View details of a service profile

    Args:
        service: str
            Service name
        profile_name: Optional[str]
            Profile to view
    """

    # Get schema to validate service
    schema = _get_service_schema(service)
    if not schema:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]Invalid service: {service}[/]")
        return

    # View the profile
    result = view(service, profile_name)

    if not result.success:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")
        return

    # Display profile information in a table
    table = Table(title=f"{service.capitalize()} Profile: {profile_name}")
    table.add_column("Key", style="cyan")
    table.add_column("Value", style="green")

    profile = result.data
    for key, value in profile.items():
        table.add_row(key, str(value))

    console.print(table)

Examples:

# View default profile
sfai config view --service mulesoft

# View specific profile
sfai config view --service mulesoft --profile-name production


delete_cmd

Delete a service profile

Parameters:

Name Type Description Default
service str

str Service name

Option(..., help='Service name')
profile_name Optional[str]

Optional[str] Profile to delete

Option('default', help='Profile to delete')
force bool

bool Delete without confirmation

Option(False, '--force', '-f', help='Delete without confirmation')

Returns:

Type Description

None

Source code in sfai/cli/config/delete.py
@app.callback(invoke_without_command=True)
def delete_cmd(
    service: str = typer.Option(..., help="Service name"),
    profile_name: Optional[str] = typer.Option("default", help="Profile to delete"),
    force: bool = typer.Option(
        False, "--force", "-f", help="Delete without confirmation"
    ),
):
    """Delete a service profile

    Args:
        service: str
            Service name
        profile_name: Optional[str]
            Profile to delete
        force: bool
            Delete without confirmation

    Returns:
        None
    """

    # Get schema to validate service
    schema = _get_service_schema(service)
    if not schema:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]Invalid service: {service}[/]")
        return

    # Ask for confirmation unless force is specified
    if not force:
        confirmed = Confirm.ask(
            f"{QUESTION_EMOJI} Are you sure you want to delete the profile "
            f"'{profile_name}' for service '{service}'?",
            default=False,
        )
        if not confirmed:
            console.print("Operation cancelled.")
            return

    # Delete the profile
    result = delete(service, profile_name)

    if result.success:
        console.print(f"{SUCCESS_EMOJI} [{SUCCESS_COLOR}]{result.message}[/]")
    else:
        console.print(f"{ERROR_EMOJI} [{ERROR_COLOR}]{result.error}[/]")

Examples:

# Delete with confirmation
sfai config delete --service mulesoft --profile-name old-profile

# Force delete without confirmation
sfai config delete --service mulesoft --profile-name old-profile --force


sfai platform - Platform Management

init_platform_cmd

Initialize or update cloud/local environment in context

Parameters:

Name Type Description Default
platform Optional[str]

str Platform type (local, eks, gcp, heroku)

Option(None, help='Platform type (local, eks, gcp, heroku)')
environment str

str Environment name (defaults to "default")

Option('default', help='Environment name')
app_name Optional[str]

Optional[str] Heroku app name (for heroku)

Option(None, help='Heroku app name (for heroku)')
team_name Optional[str]

Optional[str] Heroku team name (for heroku)

Option(None, help='Heroku team name (for heroku)')
private_space Optional[str]

Optional[str] Heroku private space name (for heroku)

Option(None, help='Heroku private space name (for heroku)')
deployment_type Optional[str]

Optional[str] Deployment type (for heroku)

Option(None, help='Deployment type (for heroku)')
routing Optional[str]

Optional[str] Routing type (for heroku)

Option(None, help='Routing type (for heroku)')
cluster_name Optional[str]

Optional[str] EKS cluster name (for eks)

Option(None, help='EKS cluster name (for eks)')
region Optional[str]

Optional[str] AWS region (for eks)

Option(None, help='AWS region (for eks)')
profile Optional[str]

Optional[str] AWS profile (for eks)

Option(None, help='AWS profile (for eks)')
namespace Optional[str]

Optional[str] namespace (for eks)

Option(None, help='namespace (for eks)')
ecr_repo Optional[str]

Optional[str] ECR repository (for eks)

Option(None, help='ECR repository (for eks)')
interactive bool

bool Interactive mode

Option(False, '-i', '--interactive', help='Interactive mode')
skip_confirm bool

bool Skip confirmation of default values

Option(False, '-y', '--yes', help='Skip confirmation of default values')
Source code in sfai/cli/platform/init.py
@app.callback(
    invoke_without_command=True,
    help="Initialize or update cloud/local environment in context",
)
def init_platform_cmd(
    platform: Optional[str] = typer.Option(
        None, help="Platform type (local, eks, gcp, heroku)"
    ),
    environment: str = typer.Option("default", help="Environment name"),
    # heroku options
    app_name: Optional[str] = typer.Option(None, help="Heroku app name (for heroku)"),
    team_name: Optional[str] = typer.Option(None, help="Heroku team name (for heroku)"),
    private_space: Optional[str] = typer.Option(
        None, help="Heroku private space name (for heroku)"
    ),
    deployment_type: Optional[str] = typer.Option(
        None, help="Deployment type (for heroku)"
    ),
    routing: Optional[str] = typer.Option(None, help="Routing type (for heroku)"),
    # eks options
    cluster_name: Optional[str] = typer.Option(None, help="EKS cluster name (for eks)"),
    region: Optional[str] = typer.Option(None, help="AWS region (for eks)"),
    profile: Optional[str] = typer.Option(None, help="AWS profile (for eks)"),
    namespace: Optional[str] = typer.Option(None, help="namespace (for eks)"),
    ecr_repo: Optional[str] = typer.Option(None, help="ECR repository (for eks)"),
    # interactive options
    interactive: bool = typer.Option(
        False, "-i", "--interactive", help="Interactive mode"
    ),
    skip_confirm: bool = typer.Option(
        False, "-y", "--yes", help="Skip confirmation of default values"
    ),
) -> None:
    """
    Initialize or update cloud/local environment in context

    Args:
        platform: str
            Platform type (local, eks, gcp, heroku)
        environment: str
            Environment name (defaults to "default")
        app_name: Optional[str]
            Heroku app name (for heroku)
        team_name: Optional[str]
            Heroku team name (for heroku)
        private_space: Optional[str]
            Heroku private space name (for heroku)
        deployment_type: Optional[str]
            Deployment type (for heroku)
        routing: Optional[str]
            Routing type (for heroku)
        cluster_name: Optional[str]
            EKS cluster name (for eks)
        region: Optional[str]
            AWS region (for eks)
        profile: Optional[str]
            AWS profile (for eks)
        namespace: Optional[str]
            namespace (for eks)
        ecr_repo: Optional[str]
            ECR repository (for eks)
        interactive: bool
            Interactive mode
        skip_confirm: bool
            Skip confirmation of default values
    """
    context = ctx_mgr.read_context()
    if not context:
        console.print(f"{ERROR_EMOJI} No app context found. Run `sfai init app` first.")
        return

    current_env = context.get("active_env", {})

    if not platform:
        platform = Prompt.ask(
            "Enter the platform type",
            choices=["eks", "heroku", "minikube"],
            default=current_env or "heroku",
        )
    environment = Prompt.ask(
        "Enter the environment",
        default=environment or context.get("environment", "default"),
    )

    if platform == "heroku":
        if interactive:
            team_name = Prompt.ask(
                "Enter the team name", default=team_name or context.get("team_name", "")
            )
            if team_name:
                private_space = Prompt.ask(
                    "Enter the private space name",
                    default=private_space or context.get("private_space", ""),
                )
            else:
                private_space = ""
            deployment_type = Prompt.ask(
                "Enter the deployment type",
                choices=["container", "buildpack"],
                default=deployment_type or context.get("deployment_type", "buildpack"),
            )
            routing = Prompt.ask(
                "Enter the routing type",
                choices=["internal", "public"],
                default=routing or context.get("routing", "public"),
            )
        else:
            app_name = app_name or context.get("app_name", "").lower()
            team_name = team_name or context.get("team_name", "")
            private_space = private_space or context.get("private_space", "")
            deployment_type = deployment_type or context.get(
                "deployment_type", "buildpack"
            )
            routing = routing or context.get("routing", "public")
            if not skip_confirm:
                console.print(
                    f"Initializing [cyan]{platform}[/] environment with the "
                    f"following values:"
                )
                console.print(f"  App Name: [cyan]{app_name}[/]")
                console.print(f"  Team Name: [cyan]{team_name or ''}[/]")
                console.print(f"  Private Space: [cyan]{private_space or ''}[/]")
                console.print(f"  Deployment Type: [cyan]{deployment_type}[/]")
                console.print(f"  Routing Type: [cyan]{routing}[/]")
                confirm = Prompt.ask(
                    "Are you sure you want to continue?",
                    choices=["y", "n"],
                    default="y",
                )
                if confirm.lower() not in ["y", "yes"]:
                    console.print(
                        f"{ERROR_EMOJI} Environment initialization cancelled."
                    )
                    return

    if platform == "eks":
        if interactive:
            cluster_name = Prompt.ask(
                "Enter the EKS cluster name",
                default=cluster_name or context.get("cluster_name"),
            )
            region = Prompt.ask(
                "Enter the region", default=region or context.get("region", "us-west-2")
            )
            profile = Prompt.ask(
                "Enter the profile",
                default=profile or context.get("profile", "default"),
            )
            namespace = Prompt.ask(
                "Enter the namespace",
                default=namespace or context.get("namespace", "default"),
            )
            ecr_repo = Prompt.ask(
                "Enter the ECR repository",
                default=ecr_repo or context.get("ecr_repo", f"{app_name}"),
            )
        else:
            cluster_name = cluster_name or context.get("cluster_name")
            region = region or context.get("region", "us-east-1")
            profile = profile or context.get("profile", "default")
            namespace = namespace or context.get("namespace", "default")
            ecr_repo = ecr_repo or context.get("ecr_repo", f"{app_name}")
            if not skip_confirm:
                console.print(
                    f"Initializing [cyan]{platform}[/] environment with the "
                    f"following values:"
                )
                console.print(f"  Cluster Name: [cyan]{cluster_name}[/]")
                console.print(f"  Region: [cyan]{region}[/]")
                console.print(f"  Profile: [cyan]{profile}[/]")
                console.print(f"  Namespace: [cyan]{namespace}[/]")
                console.print(f"  ECR Repository: [cyan]{ecr_repo}[/]")
                confirm = Prompt.ask(
                    "Are you sure you want to continue?",
                    choices=["y", "n"],
                    default="y",
                )
                if confirm.lower() not in ["y", "yes"]:
                    console.print(
                        f"{ERROR_EMOJI} Environment initialization cancelled."
                    )
                    return

    result = init(
        platform=platform,
        environment=environment,
        # heroku options
        app_name=app_name,
        team_name=team_name,
        private_space=private_space,
        deployment_type=deployment_type,
        routing=routing,
        # eks options
        cluster_name=cluster_name,
        region=region,
        profile=profile,
        namespace=namespace,
        ecr_repo=ecr_repo,
        # interactive options
        interactive=interactive,
        skip_confirm=skip_confirm,
    )

    if result.success:
        # Get the environment-specific app name from result data if available
        environment_app_name = result.data.get("app_name") if result.data else None
        app_name = environment_app_name or context.get("app_name")
        console.print(
            f"{SUCCESS_EMOJI} {platform} environment initialized → app_name: {app_name}"
        )
        if team_name:
            console.print(f"🔗 Team: {team_name}")
        if private_space:
            console.print(f"🔗 Private Space: {private_space}")
        console.print(
            f"{ROCKET_EMOJI} Next: Run `sfai app deploy` to deploy your app to "
            f"{platform}."
        )
    else:
        console.print(f"{ERROR_EMOJI} {result.error}")

Examples:

# Interactive platform initialization
sfai platform init --interactive

# Initialize Heroku platform
sfai platform init --cloud heroku --app-name my-app --team-name my-team

# Initialize with specific deployment type
sfai platform init --cloud heroku --deployment-type container --routing internal

# Initialize EKS platform
sfai platform init --cloud eks --cluster-name my-cluster --region us-west-2 --namespace production


switch_platform_cmd

Switch to a different platform and environment.

Parameters:

Name Type Description Default
platform str

str Platform to switch to (local, aws, gcp, heroku)

Argument(..., help='Platform to switch to (local, aws, gcp, heroku)')
environment str

str Environment to switch to (defaults to "default")

Option('default', help='Environment to switch to')

Returns:

Type Description
None

None

Source code in sfai/cli/platform/switch.py
@app.command(help="Switch between different configurations")
def switch_platform_cmd(
    platform: str = typer.Argument(
        ..., help="Platform to switch to (local, aws, gcp, heroku)"
    ),
    environment: str = typer.Option("default", help="Environment to switch to"),
) -> None:
    """
    Switch to a different platform and environment.

    Args:
        platform: str
            Platform to switch to (local, aws, gcp, heroku)
        environment: str
            Environment to switch to (defaults to "default")

    Returns:
        None
    """
    result = switch(platform, environment)
    if result.success:
        console.print(
            f"{SUCCESS_EMOJI} Switched to {platform} platform, {environment} "
            f"environment"
        )
    else:
        console.print(
            f"{ERROR_EMOJI} Platform '{platform}' environment '{environment}' "
            f"is not initialized yet."
        )
        console.print(
            f"{LIGHT_BULB_EMOJI} Run: sfai platform init {platform} "
            f"--environment {environment}"
        )

Examples:

# Switch to Heroku platform
sfai platform switch heroku

# Switch to local platform
sfai platform switch local

# Switch to EKS platform
sfai platform switch aws


Global Options

These options can be used with most commands:

Option Description
--help Show help message and exit
--version Show version information

Best Practices

Context Management

  • Use sfai app context to verify current app and platform settings
  • Switch platforms with sfai platform switch before deploying
  • Delete old contexts with sfai app context --delete when switching projects

Profile Management

  • Use descriptive profile names (e.g., production, staging, dev)
  • Store different environment credentials in separate profiles
  • Use sfai config list to see all configured profiles

Deployment Workflow

  1. Initialize app: sfai app init
  2. Set up platform: sfai platform init --cloud heroku
  3. Deploy: sfai app deploy
  4. Check status: sfai app status
  5. View logs: sfai app logs
  6. Open app: sfai app open

Interactive Mode

  • Use --interactive or -i for guided setup
  • Especially useful for first-time configuration
  • Provides validation and helpful prompts

Notes

  • Interactive Mode: Many commands support --interactive or -i flag for guided setup
  • Profile Management: Config commands support multiple profiles per service for different environments
  • Platform Context: App commands respect the current platform context but can be overridden with --platform
  • Required Arguments: Arguments marked as Required must be provided
  • Default Values: Default values are used when options are not specified
  • Cloudflare Tunnel: The --tunnel option in sfai app open requires cloudflared to be installed

For more examples and workflows, see the CLI Examples page.