From a4600df4d5e8a9949540a07a876e81eb2da68323 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Fri, 20 Feb 2026 20:45:30 +0100 Subject: [PATCH] feat: add init command and --project option to build --- hokusai/cli.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/hokusai/cli.py b/hokusai/cli.py index 24f8cb4..a449caa 100644 --- a/hokusai/cli.py +++ b/hokusai/cli.py @@ -23,7 +23,15 @@ class _DefaultBuildGroup(TyperGroup): # type: ignore[misc] @override def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: - if not args or (args[0] not in self.commands and not args[0].startswith("-")): + # Prepend "build" unless the first token is already a known command + # or a top-level flag (--help / --install-completion / --show-completion). + is_command = bool(args) and args[0] in self.commands + is_top_level_flag = bool(args) and args[0] in { + "--help", + "--install-completion", + "--show-completion", + } + if not is_command and not is_top_level_flag: args = ["build", *args] return super().parse_args(ctx, args) @@ -47,8 +55,24 @@ def _project_name(config_path: Path) -> str: return name -def _find_config(directory: Path) -> Path: - """Find the single ``*.hokusai.{yaml,yml}`` file in *directory*.""" +def _find_config(directory: Path, project: str | None = None) -> Path: + """Find a ``*.hokusai.{yaml,yml}`` config file in *directory*. + + When *project* is given, look for ``.hokusai.{yaml,yml}`` + specifically. Otherwise auto-detect the single config file present. + """ + if project is not None: + for suffix in _CONFIG_SUFFIXES: + candidate = directory / f"{project}{suffix}" + if candidate.exists(): + return candidate + click.echo( + click.style("Error: ", fg="red", bold=True) + + f"No config file found for project '{project}'", + err=True, + ) + raise typer.Exit(code=1) + candidates: list[Path] = [] for suffix in _CONFIG_SUFFIXES: candidates.extend(directory.glob(f"*{suffix}")) @@ -128,10 +152,13 @@ def build( target: Annotated[ str | None, typer.Argument(help="Specific target to build.") ] = None, + project: Annotated[ + str | None, typer.Option(help="Project name (loads .hokusai.yaml).") + ] = None, ) -> None: """Build all targets (or a specific target) in dependency order.""" project_dir = Path.cwd() - config_path = _find_config(project_dir) + config_path = _find_config(project_dir, project) config = load_config(config_path) name = _project_name(config_path) @@ -212,6 +239,41 @@ def graph() -> None: click.echo(f" {node}{arrow}{', '.join(preds)}") +@app.command() +def init() -> None: + """Create a starter .hokusai.yaml config file.""" + name = str(click.prompt("Project name", type=str)).strip() # pyright: ignore[reportAny] + if not name: + click.echo(click.style("Error: ", fg="red", bold=True) + "Name cannot be empty") + raise typer.Exit(code=1) + + filename = f"{name}.hokusai.yaml" + dest = Path.cwd() / filename + if dest.exists(): + click.echo( + click.style("Error: ", fg="red", bold=True) + f"{filename} already exists" + ) + raise typer.Exit(code=1) + + content = f"""\ +# {name} - hokusai project +defaults: + image_model: flux-2-pro + +targets: + great_wave.png: + prompt: >- + A recreation of Hokusai's "The Great Wave off Kanagawa", but instead of + boats and people, paint brushes, canvases, and framed paintings are + swimming and tumbling in the towering wave. Oil paint tubes burst open + and trail ribbons of colour through the spray. The iconic Mount Fuji + sits serenely in the background. Ukiyo-e woodblock print style with + vivid modern pigment colours. +""" + _ = dest.write_text(content) + click.echo(click.style(" created ", fg="green") + click.style(filename, bold=True)) + + @app.command() def models() -> None: """List available models and their capabilities."""