Skip to content

symfonic.cli.init

init

Project scaffolder -- renders the symfonic init starter template.

The scaffolder walks src/symfonic/cli/templates/ and emits every file into <output_dir>/<project_name>/. .j2 files are rendered with Jinja2 using a context derived from the CLI arguments; plain files are copied verbatim. The function is fully offline -- no HTTP calls during init.

Component filtering is handled in two complementary ways:

  1. File-level skip -- entire template files that belong exclusively to an omitted component are not written at all (e.g. app/auth/, alembic/versions/002_chats.py, webapp/).

  2. Jinja2 conditionals -- cross-cutting concerns inside a single file (e.g. router imports in app/main.py) are gated with {% if has_auth %} / {% if has_chats %} etc. so the rendered output is clean regardless of which components are selected.

ScaffolderError

Bases: RuntimeError

Raised when the scaffolder cannot complete generation.

scaffold

scaffold(
    project_name: str,
    components: list[str],
    llm_provider: str,
    output_dir: Path,
    force: bool = False,
) -> Path

Generate a starter project under <output_dir>/<project_name>/.

Parameters:

Name Type Description Default
project_name str

Directory-safe slug (letters, digits, dashes, underscores).

required
components list[str]

Subset of ALL_COMPONENTS to include.

required
llm_provider str

One of anthropic, openai, deepseek, kimi, google, ollama, aws, openrouter.

required
output_dir Path

Parent directory where the project folder will be created.

required
force bool

If True, overwrite files in an existing target directory instead of failing.

False

Returns:

Type Description
Path

The absolute path of the newly-created project directory.

Raises:

Type Description
ScaffolderError

When inputs are invalid or the target directory already exists (and force is False).

Source code in src/symfonic/cli/init.py
def scaffold(
    project_name: str,
    components: list[str],
    llm_provider: str,
    output_dir: Path,
    force: bool = False,
) -> Path:
    """Generate a starter project under ``<output_dir>/<project_name>/``.

    Args:
        project_name: Directory-safe slug (letters, digits, dashes, underscores).
        components: Subset of ``ALL_COMPONENTS`` to include.
        llm_provider: One of ``anthropic``, ``openai``, ``deepseek``, ``kimi``,
            ``google``, ``ollama``, ``aws``, ``openrouter``.
        output_dir: Parent directory where the project folder will be created.
        force: If True, overwrite files in an existing target directory instead of failing.

    Returns:
        The absolute path of the newly-created project directory.

    Raises:
        ScaffolderError: When inputs are invalid or the target directory
            already exists (and force is False).
    """
    if not _VALID_NAME.match(project_name):
        raise ScaffolderError(
            f"Invalid project name {project_name!r}: must start with a letter "
            "and contain only letters, digits, dashes, or underscores."
        )

    try:
        validate_components(components)
    except ValueError as exc:
        raise ScaffolderError(str(exc)) from exc

    output_dir = Path(output_dir).resolve()
    target = output_dir / project_name
    if target.exists() and not force:
        raise ScaffolderError(
            f"Target directory already exists: {target}. "
            "Remove it first or choose a different name."
        )

    context = _build_context(project_name, components, llm_provider)
    templates_dir = _locate_templates_dir()

    target.mkdir(parents=True, exist_ok=True)

    # File paths ending in ``.j2`` are rendered; everything else is copied.
    # Path segments themselves may contain ``{{ python_pkg }}`` tokens that
    # we substitute with the render context.
    for rel_path, src_path in _iter_template_files(templates_dir):
        # Resolve any template tokens in the path segments first so that
        # _should_skip receives a concrete relative path.
        rendered_parts = [
            _render(part, context) if ("{{" in part or "{%" in part) else part
            for part in rel_path.parts
        ]
        out_name = Path(*rendered_parts)
        if out_name.name.endswith(".j2"):
            out_name = out_name.with_name(out_name.name[: -len(".j2")])

        if _should_skip(out_name, components):
            continue

        dest = target / out_name
        dest.parent.mkdir(parents=True, exist_ok=True)

        if src_path.suffix == ".j2":
            text = src_path.read_text(encoding="utf-8")
            dest.write_text(_render(text, context), encoding="utf-8")
        else:
            shutil.copy2(src_path, dest)

    _print_next_steps(project_name, target, context)
    return target