From 7bf04ff06400023a46d5a5879413d746c3c58ffc Mon Sep 17 00:00:00 2001 From: omicron Date: Tue, 20 Oct 2026 18:05:15 +0200 Subject: [PATCH] Implement cli init command closes #1 --- omicron/ssg/cli.py | 79 ++++++++++++++++++++++++++++++++++++++++-- tests/e2e/test_init.py | 36 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/test_init.py diff --git a/omicron/ssg/cli.py b/omicron/ssg/cli.py index 063e7c6..b81e409 100644 --- a/omicron/ssg/cli.py +++ b/omicron/ssg/cli.py @@ -1,11 +1,14 @@ import argparse import logging import sys +from collections.abc import Callable from omicron.ssg.output import OutputError from omicron.ssg.site import Site -from omicron.ssg.config import ConfigError +from omicron.ssg.config import ConfigError, SiteConfig, DEFAULT_ITEMS_PER_PAGE from pathlib import Path +log = logging.getLogger(__name__) + class CliError(RuntimeError): pass @@ -46,8 +49,15 @@ def setup_argparse() -> argparse.ArgumentParser: default=False, help="include draft content", ) + + # clean command subparsers.add_parser("clean", help="remove the output directory") + # init command + subparsers.add_parser( + "init", help="create a new site directory", formatter_class=help_formatter + ) + return parser @@ -84,15 +94,80 @@ def clean(args: argparse.Namespace) -> None: site.clean() +def prompt( + label: str, + explanation: str | None = None, + default: str | None = None, + required: bool = True, + validate: Callable[[str], bool] | None = None, + error: str = "Invalid value.", +) -> str: + if explanation: + print(explanation) + hint = f" [{default}]" if default is not None else "" + while True: + value = input(f"{label}{hint}: ").strip() + if not value and default is not None: + value = default + if not value and not required: + return "" + if not value: + print("This field is required.") + continue + if validate and not validate(value): + print(error) + continue + return value + + +def init(args: argparse.Namespace) -> None: + try: + args.site.mkdir(parents=True, exist_ok=True) + except (FileExistsError, NotADirectoryError) as e: + raise CliError(f"{args.site} is not a directory") from e + if any(args.site.iterdir()): + raise CliError(f"{args.site} is not empty") + config_path = args.site / "config.yml" + + name = prompt("Site name") + base_url = prompt( + "Base URL", "Optional, e.g. https://example.com/blog", required=False + ) + template = prompt("Template", default="plain") + items_per_page = prompt( + "Items per page", + default=str(DEFAULT_ITEMS_PER_PAGE), + validate=lambda v: v.isdigit() and int(v) > 0, + error="Items per page must be a positive integer.", + ) + + config = SiteConfig.from_dict( + { + "name": name, + "base_url": base_url, + "template": template, + "items_per_page": int(items_per_page), + } + ) + Site.resolve_template_path(config.template) + + config.to_file(config_path) + (args.site / "content").mkdir(exist_ok=True) + log.info("Created new site at %s", args.site) + + def cli() -> None: parser = setup_argparse() args = parser.parse_args() - log_file = getattr(args, "log_file", args.site / "build.log") + default_log_file = None if args.command == "init" else args.site / "build.log" + log_file = getattr(args, "log_file", default_log_file) setup_logging(verbose=bool(args.verbose), log_file=log_file) if args.command == "build": build(args) elif args.command == "clean": clean(args) + elif args.command == "init": + init(args) else: raise NotImplementedError(f"'{args.command}' command not implemented yet") diff --git a/tests/e2e/test_init.py b/tests/e2e/test_init.py new file mode 100644 index 0000000..08cab39 --- /dev/null +++ b/tests/e2e/test_init.py @@ -0,0 +1,36 @@ +import subprocess +import yaml + + +def test_init_creates_site(tmp_path): + site_dir = tmp_path / "newsite" + result = subprocess.run( + ["ossg", "--site", str(site_dir), "init"], + input="My Site\nhttps://example.com\n\n\n", + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + config = yaml.safe_load((site_dir / "config.yml").read_text()) + assert config == { + "name": "My Site", + "base_url": "https://example.com/", + "template": "plain", + "items_per_page": 20, + } + assert (site_dir / "content").is_dir() + + +def test_init_refuses_non_empty_dir(tmp_path): + (tmp_path / "some_file").touch() + + result = subprocess.run( + ["ossg", "--site", str(tmp_path), "init"], + input="My Site\n\n\n\n", + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "is not empty" in result.stdout