Skip to content

Commit a03dc8f

Browse files
jawwad-aliclaude
andcommitted
fix(init): preserve interactive-cancel semantics; fold merge risk into the prompt
Two review-driven refinements to the 'init --here' non-empty confirm, keeping the maintainer-endorsed control flow (piped y/n honored; non-interactive EOF → actionable --force error): 1. typer.confirm raises typer.Abort for BOTH an interactive Ctrl+C and an EOF on closed/empty stdin. Catching it unconditionally reported 'no confirmation input available, use --force' and exited 1 even when the user cancelled at a real TTY. Branch on _stdin_is_interactive(): a TTY cancel is a normal exit 0 ('Operation cancelled'); only non-interactive EOF becomes the --force error. 2. Fold the merge-risk warning into the confirmation question instead of printing it unconditionally beforehand, so the EOF/no-input path (which exits without changing anything) no longer prints a misleading 'will be merged' line first. Adds test_init_here_interactive_cancel_exits_zero (fails before: exit 1 with --force; passes after: exit 0, 'cancelled', pre-existing file untouched). The non-interactive EOF and piped-y preserve-merge tests are unchanged and still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fc5c69a commit a03dc8f

2 files changed

Lines changed: 50 additions & 8 deletions

File tree

src/specify_cli/commands/init.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,18 +229,29 @@ def init(
229229
"[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]"
230230
)
231231
else:
232-
console.print(
233-
"[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]"
234-
)
232+
# Fold the merge risk into the confirmation prompt rather than
233+
# printing it unconditionally first: on the EOF/no-input path
234+
# below the command exits without changing anything, so a
235+
# standalone "will be merged" line would mislead. Interactive
236+
# users still see the risk as part of the question.
237+
#
235238
# Call typer.confirm normally so piped y/n is honored — e.g.
236239
# `echo y | specify init --here` keeps reaching the
237-
# non-destructive preserve-merge path. Only when no
238-
# confirmation input is available at all (closed/empty stdin
239-
# → EOF/Abort) do we convert it into an actionable error that
240-
# points at --force, rather than blocking or failing opaquely.
240+
# non-destructive preserve-merge path.
241241
try:
242-
proceed = typer.confirm("Do you want to continue?")
242+
proceed = typer.confirm(
243+
"Template files will be merged with existing content "
244+
"and may overwrite existing files. Do you want to continue?"
245+
)
243246
except (typer.Abort, EOFError):
247+
# typer.confirm raises Abort for BOTH an interactive Ctrl+C
248+
# and an EOF on closed/empty stdin. Distinguish them: a real
249+
# TTY cancellation is a normal exit (0, "cancelled"), while a
250+
# missing-input EOF (non-interactive) becomes an actionable
251+
# error pointing at --force.
252+
if _stdin_is_interactive():
253+
console.print("[yellow]Operation cancelled[/yellow]")
254+
raise typer.Exit(0) from None
244255
console.print(
245256
"[red]Error:[/red] Current directory is not empty and no "
246257
"confirmation input is available. Re-run with "

tests/integrations/test_cli.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,37 @@ def test_init_here_nonempty_noninteractive_errors_with_force_guidance(self, tmp_
141141
# Aborted before scaffolding: the pre-existing file is untouched.
142142
assert (project / "existing.txt").read_text(encoding="utf-8") == "keep me"
143143

144+
def test_init_here_interactive_cancel_exits_zero(self, tmp_path, monkeypatch):
145+
"""An interactive Ctrl+C at the merge confirmation (typer.Abort on a TTY)
146+
is a normal cancellation — exit 0, "cancelled" — NOT the missing-input
147+
--force error, which is reserved for non-interactive EOF. Guards the
148+
regression where Abort was caught unconditionally and every cancel became
149+
an exit-1 --force error."""
150+
from typer.testing import CliRunner
151+
from specify_cli import app
152+
import specify_cli.commands.init as init_mod
153+
154+
# Simulate an interactive terminal so the Abort is treated as a cancel.
155+
monkeypatch.setattr(init_mod, "_stdin_is_interactive", lambda: True)
156+
157+
project = tmp_path / "cancel-here"
158+
project.mkdir()
159+
(project / "existing.txt").write_text("keep me", encoding="utf-8")
160+
old_cwd = os.getcwd()
161+
try:
162+
os.chdir(project)
163+
# No input → typer.confirm raises Abort (stands in for Ctrl+C).
164+
result = CliRunner().invoke(app, [
165+
"init", "--here", "--integration", "copilot", "--script", "sh", "--ignore-agent-tools",
166+
], catch_exceptions=False)
167+
finally:
168+
os.chdir(old_cwd)
169+
170+
assert result.exit_code == 0, result.output
171+
assert "cancelled" in result.output.lower()
172+
assert "--force" not in result.output # not the missing-input error
173+
assert (project / "existing.txt").read_text(encoding="utf-8") == "keep me"
174+
144175
def test_integration_copilot_auto_promotes(self, tmp_path):
145176
from typer.testing import CliRunner
146177
from specify_cli import app

0 commit comments

Comments
 (0)