diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ed5b606..69afc7860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `apm install --global` now installs MCP servers to global-capable runtimes (Copilot CLI, Codex CLI) instead of blanket-skipping all MCP installation at user scope. Note: lockfile-path behavior at `--global` tracked in #794 (#638) - `--trust-transitive-mcp` no longer silently ignored when combined with `--global` (#638) - Token resolution now discriminates by port, fixing credential collisions across multiple self-hosted Git instances on the same host. Thanks @edenfunf! (#785) +- Fix `apm init` showing overwrite confirmation prompt three times on Windows CP950 terminals (#602) ## [0.8.12] - 2026-04-19 diff --git a/src/apm_cli/commands/init.py b/src/apm_cli/commands/init.py index f97ff46df..f1b7cdd57 100644 --- a/src/apm_cli/commands/init.py +++ b/src/apm_cli/commands/init.py @@ -19,7 +19,6 @@ _create_plugin_json, _get_console, _get_default_config, - _lazy_confirm, _rich_blank_line, _validate_plugin_name, _validate_project_name, @@ -84,14 +83,7 @@ def init(ctx, project_name, yes, plugin, verbose): logger.warning("apm.yml already exists") if not yes: - Confirm = _lazy_confirm() - if Confirm: - try: - confirm = Confirm.ask("Continue and overwrite?") - except Exception: - confirm = click.confirm("Continue and overwrite?") - else: - confirm = click.confirm("Continue and overwrite?") + confirm = click.confirm("Continue and overwrite?") if not confirm: logger.progress("Initialization cancelled.") diff --git a/tests/unit/test_init_command.py b/tests/unit/test_init_command.py index 135d91f02..cabde1069 100644 --- a/tests/unit/test_init_command.py +++ b/tests/unit/test_init_command.py @@ -220,6 +220,51 @@ def test_init_existing_project_interactive_cancel(self): finally: os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup + def test_init_existing_project_confirm_prompt_shown_once(self): + """Test that overwrite confirmation prompt appears exactly once (#602). + + On Windows CP950 terminals, Rich Confirm.ask() could fail on encoding, + retry internally, then fall back to click.confirm(), showing the prompt + three times. After the fix, only click.confirm() is used. + """ + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + try: + + # Create existing apm.yml + Path("apm.yml").write_text("name: existing-project\nversion: 0.1.0\n") + + # Say yes to overwrite, then provide interactive setup input + user_input = "y\nmy-project\n1.0.0\nA description\nAuthor\ny\n" + result = self.runner.invoke(cli, ["init"], input=user_input) + + assert result.exit_code == 0 + # The overwrite prompt must appear exactly once + assert result.output.count("Continue and overwrite?") == 1 + finally: + os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup + + def test_init_existing_project_confirm_uses_click(self): + """Test that overwrite confirmation uses click.confirm, not Rich (#602).""" + with tempfile.TemporaryDirectory() as tmp_dir: + os.chdir(tmp_dir) + try: + + # Create existing apm.yml + Path("apm.yml").write_text("name: existing-project\nversion: 0.1.0\n") + + with patch("apm_cli.commands.init.click.confirm", return_value=True) as mock_confirm: + result = self.runner.invoke(cli, ["init", "--yes"]) + # --yes skips the prompt entirely, so confirm should NOT be called + mock_confirm.assert_not_called() + + with patch("apm_cli.commands.init.click.confirm", return_value=False) as mock_confirm: + result = self.runner.invoke(cli, ["init"]) + mock_confirm.assert_called_once_with("Continue and overwrite?") + assert "Initialization cancelled" in result.output + finally: + os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup + def test_init_validates_project_structure(self): """Test that init creates minimal project structure.""" with tempfile.TemporaryDirectory() as tmp_dir: