feat: add init command and --project option to build

This commit is contained in:
Konstantin Fickel 2026-02-20 20:45:30 +01:00
parent d772f5dcc5
commit a4600df4d5
Signed by: kfickel
GPG key ID: A793722F9933C1A5

View file

@ -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 ``<project>.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 <project>.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."""