Просмотр исходного кода

fix(cli): ship v0.2.0-1 hotfix

xcad 2 месяцев назад
Родитель
Сommit
4b28b635fb

+ 6 - 0
CHANGELOG.md

@@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Default library migration now rewrites the built-in repository from `christianlempa/boilerplates` to `christianlempa/boilerplates-library`, with startup notices and legacy `/library` path fallback (#1762)
 - Variable definitions now use the `secret` type and nested `config` metadata for options, placeholders, sliders, and secret autogeneration across the runtime, schemas, and migrated template specs (#1767)
 
+## [0.2.0-1] - 2026-04-23
+
+### Fixed
+- Interactive prompts now show and preserve effective defaults loaded from config, var-files, and CLI overrides.
+- `generate --dry-run` no longer prompts for local versus remote destinations when none are provided and now ends with a dry-run-specific success message.
+
 ## [0.1.2] - 2025-12-11
 
 ### Fixed

+ 1 - 1
cli/__init__.py

@@ -2,6 +2,6 @@
 Boilerplates CLI - A sophisticated command-line tool for managing infrastructure boilerplates.
 """
 
-__version__ = "0.2.0"
+__version__ = "0.2.0-1"
 __author__ = "Christian Lempa"
 __description__ = "CLI tool for managing infrastructure boilerplates"

+ 2 - 1
cli/core/input/prompt_manager.py

@@ -120,7 +120,8 @@ class PromptHandler:
         prompt_text = variable.get_prompt_text()
         default_value = variable.get_normalized_default()
         has_explicit_default = "default" in variable._explicit_fields or "value" in variable._explicit_fields
-        if not has_explicit_default and not variable.autogenerated and not variable.is_required():
+        has_applied_default = variable.origin in {"config", "var-file", "cli"}
+        if not has_explicit_default and not has_applied_default and not variable.autogenerated and not variable.is_required():
             default_value = None
 
         # Add lock icon before default value for secret or autogenerated variables

+ 12 - 3
cli/core/module/base_commands.py

@@ -419,6 +419,7 @@ def generate_template(module_instance, config: GenerationConfig) -> None:  # noq
     display = DisplayManager(quiet=config.quiet) if config.quiet else module_instance.display
     template = _prepare_template(module_instance, config.id, config.var_file, config.var, display)
     slug = getattr(template, "slug", template.id)
+    used_implicit_dry_run_destination = False
 
     try:
         destination = resolve_cli_destination(config.output, config.remote, config.remote_path, slug)
@@ -439,7 +440,10 @@ def generate_template(module_instance, config: GenerationConfig) -> None:  # noq
         rendered_files, _variable_values = _render_template(template, config.id, display, config.interactive)
 
         if destination is None:
-            if config.interactive:
+            if config.dry_run:
+                destination = GenerationDestination(mode="local", local_output_dir=Path.cwd() / slug)
+                used_implicit_dry_run_destination = True
+            elif config.interactive:
                 destination = prompt_generation_destination(slug)
             else:
                 destination = GenerationDestination(mode="local", local_output_dir=Path.cwd() / slug)
@@ -447,7 +451,8 @@ def generate_template(module_instance, config: GenerationConfig) -> None:  # noq
         if not destination.is_remote:
             output_dir = destination.local_output_dir or (Path.cwd() / slug)
             if (
-                not config.quiet
+                not config.dry_run
+                and not config.quiet
                 and check_output_directory(output_dir, rendered_files, config.interactive, display) is None
             ):
                 return
@@ -494,7 +499,11 @@ def generate_template(module_instance, config: GenerationConfig) -> None:  # noq
                     display.success(f"Boilerplate uploaded successfully to '{remote_target}'")
             elif config.dry_run and dry_run_stats:
                 total_files, overwrite_files, size_str = dry_run_stats
-                if overwrite_files > 0:
+                if used_implicit_dry_run_destination:
+                    display.success(
+                        f"Dry run complete: boilerplate rendered successfully ({total_files} files, {size_str}, preview only)"
+                    )
+                elif overwrite_files > 0:
                     display.warning(
                         f"Dry run complete: {total_files} files ({size_str}) would be written to '{output_dir}' "
                         f"({overwrite_files} would be overwritten)"

+ 1 - 1
pyproject.toml

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "boilerplates"
-version = "0.2.0"
+version = "0.2.0-1"
 description = "CLI tool for managing infrastructure boilerplates"
 readme = "README.md"
 requires-python = ">=3.9"

+ 53 - 1
tests/test_base_commands.py

@@ -4,17 +4,34 @@ from __future__ import annotations
 
 from types import SimpleNamespace
 
-from cli.core.module.base_commands import list_templates
+from cli.core.module.base_commands import GenerationConfig, generate_template, list_templates
 
 
 class _DisplayCapture:
     def __init__(self) -> None:
         self.lines: list[str] = []
+        self.templates = SimpleNamespace(
+            render_template_header=lambda *args, **kwargs: None,
+            render_file_tree=lambda *args, **kwargs: None,
+        )
+        self.variables = SimpleNamespace(render_variables_table=lambda *args, **kwargs: None)
 
     def text(self, value: str, style: str | None = None) -> None:
         del style
         self.lines.append(value)
 
+    def success(self, value: str, *args, **kwargs) -> None:
+        del args, kwargs
+        self.lines.append(value)
+
+    def warning(self, value: str, *args, **kwargs) -> None:
+        del args, kwargs
+        self.lines.append(value)
+
+    def error(self, value: str, *args, **kwargs) -> None:
+        del args, kwargs
+        self.lines.append(value)
+
     def data_table(self, *args, **kwargs) -> None:
         del args, kwargs
         raise AssertionError("data_table should not be used for raw output")
@@ -47,3 +64,38 @@ def test_list_templates_raw_outputs_tab_separated_rows() -> None:
 
     assert returned_templates == [template]
     assert display.lines == ["whoami\tWhoami\tdocker,test\t1.0.0\tdefault"]
+
+
+def test_generate_template_dry_run_skips_destination_prompt_and_overwrite_check(
+    monkeypatch,
+) -> None:
+    """Dry runs without explicit destinations should not ask where to write or confirm overwrites."""
+    display = _DisplayCapture()
+    template = SimpleNamespace(id="whoami", slug="whoami")
+    module_instance = SimpleNamespace(name="compose", display=display)
+
+    monkeypatch.setattr("cli.core.module.base_commands._prepare_template", lambda *args, **kwargs: template)
+    monkeypatch.setattr(
+        "cli.core.module.base_commands._render_template",
+        lambda *args, **kwargs: ({"compose.yaml": "services:\n"}, {}),
+    )
+    monkeypatch.setattr(
+        "cli.core.module.base_commands.prompt_generation_destination",
+        lambda slug: (_ for _ in ()).throw(AssertionError(f"prompt_generation_destination called for {slug}")),
+    )
+    monkeypatch.setattr(
+        "cli.core.module.base_commands.check_output_directory",
+        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("check_output_directory should not run")),
+    )
+
+    generate_template(
+        module_instance,
+        GenerationConfig(
+            id="whoami",
+            interactive=True,
+            dry_run=True,
+        ),
+    )
+
+    assert any("boilerplate rendered successfully" in line for line in display.lines)
+    assert any("preview only" in line for line in display.lines)

+ 60 - 0
tests/test_prompt_manager.py

@@ -66,3 +66,63 @@ def test_required_integer_without_default_still_uses_integer_prompt(monkeypatch:
 
     assert prompt_handler._prompt_variable(variable) == TEST_REQUIRED_INTEGER
     assert calls
+
+
+@pytest.mark.parametrize("origin", ["config", "var-file", "cli"])
+def test_applied_optional_string_defaults_are_shown_and_preserved(
+    monkeypatch: pytest.MonkeyPatch,
+    origin: str,
+) -> None:
+    """Applied defaults from external sources should be visible in prompts and kept on Enter."""
+    prompts: list[dict[str, Any]] = []
+
+    def fake_ask(prompt_text: str, default: str = "", show_default: bool = False, **kwargs: Any) -> str:
+        prompts.append(
+            {
+                "prompt_text": prompt_text,
+                "default": default,
+                "show_default": show_default,
+                **kwargs,
+            }
+        )
+        return default
+
+    monkeypatch.setattr("cli.core.input.prompt_manager.Prompt.ask", fake_ask)
+
+    variable = Variable({"name": "service_name", "type": "str"})
+    variable.value = "grafana"
+    variable.origin = origin
+
+    prompt_handler = PromptHandler()
+    raw_value = prompt_handler._prompt_variable(variable)
+
+    assert raw_value == "grafana"
+    assert prompts
+    assert prompts[0]["default"] == "grafana"
+    assert prompts[0]["show_default"] is True
+
+
+@pytest.mark.parametrize("origin", ["config", "var-file", "cli"])
+def test_applied_optional_bool_defaults_are_shown_and_preserved(
+    monkeypatch: pytest.MonkeyPatch,
+    origin: str,
+) -> None:
+    """Applied boolean defaults should use confirm prompts with the current value as default."""
+    calls: list[tuple[str, Any]] = []
+
+    def fake_confirm(self, prompt: str, default: bool | None = None) -> bool:
+        del self
+        calls.append((prompt, default))
+        assert default is True
+        return default
+
+    monkeypatch.setattr("cli.core.input.prompt_manager.InputManager.confirm", fake_confirm)
+
+    variable = Variable({"name": "traefik_enabled", "type": "bool"})
+    variable.value = True
+    variable.origin = origin
+
+    prompt_handler = PromptHandler()
+
+    assert prompt_handler._prompt_variable(variable) is True
+    assert calls