Skip to content

Commit cd36c9b

Browse files
jawwad-aliclaude
andcommitted
fix(init): don't block on confirmation for 'init --here' without a TTY
When 'specify init --here' targets a non-empty directory without --force, it called typer.confirm() unconditionally. In a non-interactive session (no TTY -- CI, piped, agent) there is no input, so the prompt reads EOF and aborts unhelpfully (or blocks), with no actionable message. The named-project path already fails fast and points to --force; --here was the inconsistent outlier. Guard the confirmation with the existing _stdin_is_interactive() helper: when non-interactive, print a clear 'directory not empty; re-run with --force' error and exit 1 instead of prompting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5bdcb4a commit cd36c9b

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

src/specify_cli/commands/init.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ def init(
229229
console.print(
230230
"[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]"
231231
)
232+
elif not _stdin_is_interactive():
233+
# No TTY to confirm on: fail fast with actionable guidance
234+
# instead of blocking on typer.confirm (which would read EOF
235+
# and abort unhelpfully). Mirrors the named-project path,
236+
# which already errors and points to --force.
237+
console.print(
238+
"[red]Error:[/red] Current directory is not empty and no "
239+
"interactive terminal is available to confirm. Re-run with "
240+
"[bold]--force[/bold] to merge into it."
241+
)
242+
raise typer.Exit(1)
232243
else:
233244
response = typer.confirm("Do you want to continue?")
234245
if not response:

tests/integrations/test_cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,34 @@ def fail_select(*_args, **_kwargs):
121121
data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8"))
122122
assert data["integration"] == specify_cli.DEFAULT_INIT_INTEGRATION
123123

124+
def test_init_here_nonempty_noninteractive_errors_with_force_guidance(self, tmp_path, monkeypatch):
125+
"""`init --here` on a non-empty directory must not block on a confirmation
126+
prompt when there is no interactive terminal: it should fail fast with
127+
guidance to use --force, instead of reading EOF and aborting unhelpfully."""
128+
from typer.testing import CliRunner
129+
from specify_cli import app
130+
from specify_cli.commands import init as init_mod
131+
132+
# Deterministically exercise the non-interactive branch.
133+
monkeypatch.setattr(init_mod, "_stdin_is_interactive", lambda: False)
134+
135+
project = tmp_path / "nonempty-here"
136+
project.mkdir()
137+
(project / "existing.txt").write_text("keep me", encoding="utf-8")
138+
old_cwd = os.getcwd()
139+
try:
140+
os.chdir(project)
141+
result = CliRunner().invoke(app, [
142+
"init", "--here", "--integration", "copilot", "--script", "sh", "--ignore-agent-tools",
143+
], catch_exceptions=False)
144+
finally:
145+
os.chdir(old_cwd)
146+
147+
assert result.exit_code == 1, result.output
148+
assert "--force" in result.output
149+
# Aborted before scaffolding: the pre-existing file is untouched.
150+
assert (project / "existing.txt").read_text(encoding="utf-8") == "keep me"
151+
124152
def test_integration_copilot_auto_promotes(self, tmp_path):
125153
from typer.testing import CliRunner
126154
from specify_cli import app

0 commit comments

Comments
 (0)