Skip to content

Commit b753d60

Browse files
Sergio SisternesCopilot
andcommitted
fix(init): remove triple confirm prompt on Windows CP950 terminals (#602)
Replace Rich Confirm.ask() with click.confirm() for the overwrite prompt in 'apm init'. Rich's internal retry on encoding errors caused the prompt to appear up to three times on Windows CP950 terminals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 860c608 commit b753d60

File tree

3 files changed

+48
-9
lines changed

3 files changed

+48
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- `apm install` no longer silently drops skills, agents, and commands when a Claude Code plugin also ships `hooks/*.json`. The package-type detection cascade now classifies plugin-shaped packages as `MARKETPLACE_PLUGIN` (which already maps hooks via the plugin synthesizer) before falling back to the hook-only classification, and emits a default-visibility `[!]` warning when a hook-only classification disagrees with the package's directory contents (#780)
2929
- Preserve custom git ports across protocols: non-default ports on `ssh://` and `https://` dependency URLs (e.g. Bitbucket Datacenter on SSH port 7999, self-hosted GitLab on HTTPS port 8443) are now captured as a first-class `port` field on `DependencyReference` and threaded through all clone URL builders. When the SSH clone fails, the HTTPS fallback reuses the same port instead of silently dropping it (#661, #731)
3030
- Detect port-like first path segment in SCP shorthand (`git@host:7999/path`) and raise an actionable error suggesting the `ssh://` URL form, instead of silently misparsing the port as part of the repository path (#784)
31+
- Fix `apm init` showing overwrite confirmation prompt three times on Windows CP950 terminals (#602)
32+
- Propagate headers and environment variables through OpenCode MCP adapter with defensive copies to prevent mutation (#622)
3133

3234
## [0.8.12] - 2026-04-19
3335

src/apm_cli/commands/init.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
_create_plugin_json,
2020
_get_console,
2121
_get_default_config,
22-
_lazy_confirm,
2322
_rich_blank_line,
2423
_validate_plugin_name,
2524
_validate_project_name,
@@ -84,14 +83,7 @@ def init(ctx, project_name, yes, plugin, verbose):
8483
logger.warning("apm.yml already exists")
8584

8685
if not yes:
87-
Confirm = _lazy_confirm()
88-
if Confirm:
89-
try:
90-
confirm = Confirm.ask("Continue and overwrite?")
91-
except Exception:
92-
confirm = click.confirm("Continue and overwrite?")
93-
else:
94-
confirm = click.confirm("Continue and overwrite?")
86+
confirm = click.confirm("Continue and overwrite?")
9587

9688
if not confirm:
9789
logger.progress("Initialization cancelled.")

tests/unit/test_init_command.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,51 @@ def test_init_existing_project_interactive_cancel(self):
220220
finally:
221221
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup
222222

223+
def test_init_existing_project_confirm_prompt_shown_once(self):
224+
"""Test that overwrite confirmation prompt appears exactly once (#602).
225+
226+
On Windows CP950 terminals, Rich Confirm.ask() could fail on encoding,
227+
retry internally, then fall back to click.confirm(), showing the prompt
228+
three times. After the fix, only click.confirm() is used.
229+
"""
230+
with tempfile.TemporaryDirectory() as tmp_dir:
231+
os.chdir(tmp_dir)
232+
try:
233+
234+
# Create existing apm.yml
235+
Path("apm.yml").write_text("name: existing-project\nversion: 0.1.0\n")
236+
237+
# Say yes to overwrite, then provide interactive setup input
238+
user_input = "y\nmy-project\n1.0.0\nA description\nAuthor\ny\n"
239+
result = self.runner.invoke(cli, ["init"], input=user_input)
240+
241+
assert result.exit_code == 0
242+
# The overwrite prompt must appear exactly once
243+
assert result.output.count("Continue and overwrite?") == 1
244+
finally:
245+
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup
246+
247+
def test_init_existing_project_confirm_uses_click(self):
248+
"""Test that overwrite confirmation uses click.confirm, not Rich (#602)."""
249+
with tempfile.TemporaryDirectory() as tmp_dir:
250+
os.chdir(tmp_dir)
251+
try:
252+
253+
# Create existing apm.yml
254+
Path("apm.yml").write_text("name: existing-project\nversion: 0.1.0\n")
255+
256+
with patch("apm_cli.commands.init.click.confirm", return_value=True) as mock_confirm:
257+
result = self.runner.invoke(cli, ["init", "--yes"])
258+
# --yes skips the prompt entirely, so confirm should NOT be called
259+
mock_confirm.assert_not_called()
260+
261+
with patch("apm_cli.commands.init.click.confirm", return_value=False) as mock_confirm:
262+
result = self.runner.invoke(cli, ["init"])
263+
mock_confirm.assert_called_once_with("Continue and overwrite?")
264+
assert "Initialization cancelled" in result.output
265+
finally:
266+
os.chdir(self.original_dir) # restore CWD before TemporaryDirectory cleanup
267+
223268
def test_init_validates_project_structure(self):
224269
"""Test that init creates minimal project structure."""
225270
with tempfile.TemporaryDirectory() as tmp_dir:

0 commit comments

Comments
 (0)