diff --git a/pyproject.toml b/pyproject.toml index ba7b207..12b8a82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ ] [project.scripts] -quickbot = "quickbot_cli.cli:main" +quickbot = "quickbot_cli.cli:app" [project.optional-dependencies] dev = [ diff --git a/src/quickbot_cli/cli.py b/src/quickbot_cli/cli.py index bcf2277..879cedc 100644 --- a/src/quickbot_cli/cli.py +++ b/src/quickbot_cli/cli.py @@ -14,7 +14,7 @@ import typer import yaml from jinja2 import Environment, FileSystemLoader, StrictUndefined -app = typer.Typer(help="Project scaffolding CLI") +app = typer.Typer(help="QuickBot CLI") TEMPLATES_DIR = Path(__file__).parent / "templates" # Module-level constants @@ -83,7 +83,7 @@ def _handle_boolean_choices(prompt: str, choices: list[bool], *, default: bool | typer.Exit: If invalid input is provided """ - raw = typer.prompt(f"{prompt} {choices} (default: {default})", default=default) + raw = typer.prompt(f"{prompt} [y/n]", default=default) coerced = _to_bool_like(value=raw) if coerced is None: @@ -110,7 +110,7 @@ def _handle_regular_choices(prompt: str, choices: list[str], default: str | None typer.Exit: If invalid input is provided """ - val: str = typer.prompt(f"{prompt} {choices} (default: {default})", default=default) + val: str = typer.prompt(f"{prompt} {choices}", default=default) if val not in choices: typer.secho(f"Value must be one of {choices}", fg=typer.colors.RED) raise typer.Exit(code=1) @@ -141,9 +141,7 @@ def ask_variables(spec: dict[str, Any], non_interactive: dict[str, Any]) -> dict choices = meta.get("choices") if choices: - # Detect boolean-choice variables and coerce input accordingly - is_boolean_choices = all(isinstance(c, bool) for c in choices) - if is_boolean_choices: + if all(isinstance(c, bool) for c in choices): val = _handle_boolean_choices(prompt=prompt, choices=choices, default=default) else: val = _handle_regular_choices(prompt=prompt, choices=choices, default=default) @@ -189,6 +187,9 @@ def render_tree( for item in template_root.iterdir(): if item.is_file(): + # Skip __template__.yaml + if item.name == "__template__.yaml": + continue if item.suffix == ".j2": # Render template file output_file = output_dir / item.stem @@ -318,16 +319,24 @@ def _init_project( @app.command() def init( - output: Path = typer.Option(".", help="Output directory (defaults to current directory)"), - template: str = typer.Option(DEFAULT_TEMPLATE, "--template", "-t"), - project_name: str | None = typer.Option(None, help="Project name"), - description: str | None = typer.Option(None, help="Description"), - author: str | None = typer.Option(None, help="Author"), - license_name: str | None = typer.Option(None, help="License"), + output: Path = typer.Option(".", help="Output directory (defaults to current directory)", show_default=False), + template: str = typer.Option(DEFAULT_TEMPLATE, "--template", "-t", hidden=True), + project_name: str | None = typer.Option(None, help="Project name", show_default=False), + description: str | None = typer.Option(None, help="Description", show_default=False), + author: str | None = typer.Option(None, help="Author", show_default=False), + license_name: str | None = typer.Option(None, help="License", show_default=False), *, - include_alembic: bool | None = typer.Option(None, help="Include Alembic (will prompt if not specified)"), - include_i18n: bool | None = typer.Option(None, help="Include i18n (will prompt if not specified)"), + include_alembic: bool | None = typer.Option( + None, help="Include Alembic (will prompt if not specified)", show_default=False + ), + include_i18n: bool | None = typer.Option( + None, help="Include i18n (will prompt if not specified)", show_default=False + ), overwrite: bool = typer.Option(default=False, help="Overwrite existing files"), + interactive: bool = typer.Option( + default=True, + help="Interactive mode (if interactive is disabled, defaults will be taken from __template__.yaml)", + ), ) -> None: """Initialize a new project in the specified directory.""" _init_project( @@ -340,7 +349,7 @@ def init( include_alembic=include_alembic, include_i18n=include_i18n, overwrite=overwrite, - interactive=True, + interactive=interactive, ) diff --git a/src/quickbot_cli/templates/basic/.env.j2 b/src/quickbot_cli/templates/basic/.env.j2 index 979d908..7304968 100644 --- a/src/quickbot_cli/templates/basic/.env.j2 +++ b/src/quickbot_cli/templates/basic/.env.j2 @@ -20,7 +20,7 @@ TELEGRAM_WEBHOOK_DOMAIN = "example.com" TELEGRAM_WEBHOOK_SCHEME = "https" TELEGRAM_WEBHOOK_PORT = 443 TELEGRAM_WEBHOOK_AUTH_KEY = "changethis" -TELEGRAM_BOT_TOKEN "changethis" +TELEGRAM_BOT_TOKEN = "changethis" TELEGRAM_BOT_SERVER = "https://api.telegram.org" TELEGRAM_BOT_SERVER_IS_LOCAL = False diff --git a/tests/test_cli.py b/tests/test_cli.py index 4b7de22..7867f95 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -630,6 +630,41 @@ post_tasks: assert (out3 / "alembic").exists() assert not (out3 / "locales").exists() + def test_init_command_project_name_fallback(self, temp_dir: Path) -> None: + """Test that project_name falls back to output directory name when not provided.""" + with patch("quickbot_cli.cli.TEMPLATES_DIR", temp_dir / "templates"): + # Create template structure + template_dir = temp_dir / "templates" / "basic" + template_dir.mkdir(parents=True) + + # Create template spec WITHOUT project_name variable + spec_file = template_dir / "__template__.yaml" + spec_file.write_text("variables:\n description:\n prompt: Description\n default: A test project") + + # Create template files + (template_dir / "app").mkdir() + (template_dir / "app" / "main.py.j2").write_text("app = FastAPI(title='{{ project_name }}')") + + # Test with output directory that has a name (to test the fallback) + output_path = temp_dir / "my_project_output" + _init_project( + output=output_path, + template="basic", + project_name=None, # Explicitly set to None to trigger fallback + description="A test project", + author="Test Author", + license_name="MIT", + include_alembic=False, + include_i18n=False, + overwrite=False, + interactive=False, # Disable interactive mode to avoid prompting + ) + + # Verify that project_name was set to the output directory name + # This tests the fallback logic in line 297 + assert output_path.exists() + # The project_name should be "my_project_output" (the directory name) + class TestCLIHelp: """Test CLI help and argument parsing.""" @@ -637,7 +672,7 @@ class TestCLIHelp: def test_cli_help(self, cli_runner: CliRunner) -> None: """Test that CLI shows help information.""" # Test the actual CLI interface - result = cli_runner.invoke(app, ["--help"]) + result = cli_runner.invoke(app, ["--help"], env={"NO_COLOR": "1", "COLUMNS": "120"}) assert result.exit_code == 0 # Check for the actual help text that appears assert "init" in result.output @@ -646,7 +681,7 @@ class TestCLIHelp: def test_init_command_help(self, cli_runner: CliRunner) -> None: """Test that init command shows help information.""" # Test the actual CLI interface - result = cli_runner.invoke(app, ["init", "--help"]) + result = cli_runner.invoke(app, ["init", "--help"], env={"NO_COLOR": "1", "COLUMNS": "120"}) assert result.exit_code == 0 # Check for the actual help text that appears assert "--output" in result.output @@ -655,7 +690,7 @@ class TestCLIHelp: def test_init_command_arguments(self, cli_runner: CliRunner) -> None: """Test that init command accepts required arguments.""" # Test the actual CLI interface - result = cli_runner.invoke(app, ["init", "--help"]) + result = cli_runner.invoke(app, ["init", "--help"], env={"NO_COLOR": "1", "COLUMNS": "120"}) assert result.exit_code == 0 assert "--output" in result.output @@ -702,6 +737,14 @@ class TestCLIHelp: assert hasattr(init, "__name__") assert init.__name__ == "init" + def test_version_command(self, cli_runner: CliRunner) -> None: + """Test that version command works and shows version information.""" + # Test the actual CLI interface + result = cli_runner.invoke(app, ["version"], env={"NO_COLOR": "1", "COLUMNS": "120"}) + assert result.exit_code == 0 + # Check that it shows version information + assert "quickbot-cli version" in result.output + class TestCLIOverwriteParsing: """Test overwrite string parsing through the init function (covers conversion)."""