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

This commit is contained in:
xcad
2026-04-23 11:09:38 +02:00
parent 5fa4a6f211
commit 4b28b635fb
7 changed files with 135 additions and 7 deletions
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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