Skip to content

symfonic.agent.cli.init_command

init_command

The symfonic init command body.

Split out of :mod:symfonic.agent.cli.main (389 lines against the 300-line budget). init is 150 of those on its own -- the option surface plus the interactive prompt flow -- and it is the one command in the app that never touches the agent runtime.

This follows the pattern main already uses for doctor: the body lives in its own module and main registers it with app.command(), so the command surface is unchanged and the body stays unit-testable without typer's app wiring.

init

init(project_name: Annotated[str | None, Argument(help='Project name (directory-safe slug). Not required with --list-components or --interactive / -i.')] = None, components: Annotated[str, Option('--components', help='Comma-separated list of components to include. Example: --components fastapi,auth,webapp. Default: all components.')] = _DEFAULT_COMPONENTS, list_components: Annotated[bool, Option('--list-components/--no-list-components', help='Print available components and exit.', show_default=False)] = False, llm_provider: Annotated[str, Option('--llm-provider', help='Default LLM provider: anthropic, openai, deepseek, kimi, google, ollama, aws, openrouter.')] = 'anthropic', output_dir: Annotated[Path, Option('--output-dir', help='Parent directory under which <project_name> will be created.')] = Path('.'), interactive: Annotated[bool, Option('--interactive', '-i', help='Run interactive scaffolding prompts.', show_default=False)] = False, force: Annotated[bool, Option('--force', '-f', help='Force scaffolding even if the target directory already exists.', show_default=False)] = False) -> None

Scaffold a new symfonic-powered project.

Pass --list-components to print the available component names and exit.

Source code in src/symfonic/agent/cli/init_command.py
def init(
    project_name: Annotated[
        str | None,
        typer.Argument(
            help="Project name (directory-safe slug). "
            "Not required with --list-components or --interactive / -i.",
        ),
    ] = None,
    components: Annotated[
        str,
        typer.Option(
            "--components",
            help=(
                "Comma-separated list of components to include. "
                "Example: --components fastapi,auth,webapp. "
                "Default: all components."
            ),
        ),
    ] = _DEFAULT_COMPONENTS,
    list_components: Annotated[
        bool,
        typer.Option(
            "--list-components/--no-list-components",
            help="Print available components and exit.",
            show_default=False,
        ),
    ] = False,
    llm_provider: Annotated[
        str,
        typer.Option(
            "--llm-provider",
            help=(
                "Default LLM provider: anthropic, openai, deepseek, kimi, "
                "google, ollama, aws, openrouter."
            ),
        ),
    ] = "anthropic",
    output_dir: Annotated[
        Path,
        typer.Option(
            "--output-dir",
            help="Parent directory under which <project_name> will be created.",
        ),
    ] = Path("."),
    interactive: Annotated[
        bool,
        typer.Option(
            "--interactive",
            "-i",
            help="Run interactive scaffolding prompts.",
            show_default=False,
        ),
    ] = False,
    force: Annotated[
        bool,
        typer.Option(
            "--force",
            "-f",
            help="Force scaffolding even if the target directory already exists.",
            show_default=False,
        ),
    ] = False,
) -> None:
    """Scaffold a new symfonic-powered project.

    Pass --list-components to print the available component names and exit.
    """
    from symfonic.cli.components import ALL_COMPONENTS, COMPONENT_CATALOG

    if list_components:
        typer.echo("Available components:\n")
        for name in ALL_COMPONENTS:
            typer.echo(f"  {name:<20} {COMPONENT_CATALOG[name]}")
        typer.echo(f"\nAll components: {', '.join(ALL_COMPONENTS)}")
        typer.echo(
            f"Default (no --components flag): {_DEFAULT_COMPONENTS}"
            "\n  Example domains are opt-in and mutually exclusive with the generic"
            "\n  starter; add one explicitly, e.g. --components fastapi,webapp,store-starter"
        )
        raise typer.Exit(0)

    # Expand comma-separated component names.
    if interactive:
        if not project_name:
            project_name = typer.prompt("Project name")
            if not project_name or not project_name.strip():
                typer.echo("error: project-name is required.", err=True)
                raise SystemExit(1)

        llm_provider = typer.prompt(
            "Default LLM provider (anthropic, openai, deepseek, kimi, google, "
            "ollama, aws, openrouter)",
            default=llm_provider,
        )

        customize = typer.confirm(
            "Would you like to customize the selected components?",
            default=False,
        )
        if customize:
            selected = []
            for name in ALL_COMPONENTS:
                desc = COMPONENT_CATALOG[name]
                if typer.confirm(f"Include {name} ({desc})?", default=True):
                    selected.append(name)
            if not selected:
                typer.echo("warning: no components selected, using fastapi only.", err=True)
                selected = ["fastapi"]
        else:
            selected = [c.strip() for c in components.split(",") if c.strip()]
    else:
        if not project_name:
            typer.echo(
                "error: project-name is required. "
                "Usage: symfonic init <project-name> [--components ...]",
                err=True,
            )
            raise SystemExit(1)
        selected = [c.strip() for c in components.split(",") if c.strip()]

    # Validate early so the error message comes from the CLI layer.
    unknown = [c for c in selected if c not in set(ALL_COMPONENTS)]
    if unknown:
        typer.echo(
            f"error: unknown component(s): {unknown}. "
            "Run `symfonic init --list-components` to see valid names.",
            err=True,
        )
        raise SystemExit(1)

    try:
        from symfonic.cli.init import ScaffolderError, scaffold
    except ImportError as exc:  # pragma: no cover
        typer.echo(
            "The init command requires jinja2. Install it with: "
            "pip install symfonic-core[init]",
            err=True,
        )
        raise SystemExit(1) from exc

    try:
        scaffold(
            project_name=project_name,
            components=selected,
            llm_provider=llm_provider,
            output_dir=output_dir,
            force=force,
        )
    except ScaffolderError as exc:
        typer.echo(f"error: {exc}", err=True)
        raise SystemExit(1) from exc