Skip to content

fix(init): don't block on confirmation for 'init --here' without a TTY#3236

Open
jawwad-ali wants to merge 5 commits into
github:mainfrom
jawwad-ali:fix/init-here-noninteractive
Open

fix(init): don't block on confirmation for 'init --here' without a TTY#3236
jawwad-ali wants to merge 5 commits into
github:mainfrom
jawwad-ali:fix/init-here-noninteractive

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Description

When specify init --here targets a non-empty directory and --force is not given, it called typer.confirm("Do you want to continue?") unconditionally. In a non-interactive session (CI, piped input, an agent driving the CLI) there is no TTY, so the prompt reads EOF and aborts unhelpfully (or blocks waiting for input) — with no actionable message.

The named-project path already handles this correctly: it fails fast with an error panel pointing to --force. The --here path was the inconsistent outlier, even though the module already has a _stdin_is_interactive() helper used elsewhere.

Fix

Guard the confirmation with _stdin_is_interactive(): when non-interactive, print a clear "directory is not empty … re-run with --force to merge" error and exit 1 instead of prompting. Interactive behavior and the --force fast-path are unchanged.

Testing

  • uvx ruff check clean.
  • New test_init_here_nonempty_noninteractive_errors_with_force_guidance: a non-empty cwd + init --here (no --force) under a non-interactive stdin now exits 1 with --force guidance and leaves the pre-existing file untouched. Fails/blocks before, passes after.
  • The existing test_noninteractive_init_defaults_to_copilot still passes.

AI Disclosure

  • I did use AI assistance (describe below)

Found and fixed with Claude Code (Claude Opus 4.8) under my direction. AI spotted the unguarded typer.confirm on the --here path versus the already-guarded named-project path; I confirmed the _stdin_is_interactive helper's existing use, verified the fix and that scaffolding is skipped on abort, and reviewed the diff before submitting.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes specify init --here behavior in non-interactive sessions by avoiding an unconditional confirmation prompt when the current directory is non-empty (and --force is not provided). It aligns the --here path with the existing named-project path by failing fast with actionable --force guidance instead of blocking/EOF-aborting.

Changes:

  • Guard typer.confirm(...) in the init --here non-empty directory flow with _stdin_is_interactive() and exit with a clear --force message when non-interactive.
  • Add a CLI-level regression test covering init --here in a non-empty directory under a forced non-interactive stdin.
Show a summary per file
File Description
src/specify_cli/commands/init.py Adds a non-interactive guard to prevent blocking/EOF behavior and provide --force guidance for init --here into non-empty dirs.
tests/integrations/test_cli.py Adds a regression test ensuring non-interactive init --here into a non-empty directory exits 1 with --force guidance and preserves existing files.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread src/specify_cli/commands/init.py Outdated
@jawwad-ali jawwad-ali force-pushed the fix/init-here-noninteractive branch from cd36c9b to b4a6fcd Compare June 29, 2026 18:35
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

Fixed the failing pytest (ubuntu, 3.11) leg in b4a6fcd. The first revision guarded the confirmation on _stdin_is_interactive() (isatty), which short-circuited before typer.confirm and wrongly rejected piped confirmation (e.g. echo y | specify init --here), regressing test_init_here_without_force_preserves_shared_infra.

The revised fix calls typer.confirm normally — piped y/n is honored — and catches the Abort/EOFError it raises only when stdin is empty, converting that to the actionable --force guidance. Both test_init_here_without_force_preserves_shared_infra and the new no-input test pass locally; ruff clean.

AI disclosure: prepared with Claude Code (Claude Opus 4.8) under my direction; I reviewed the diff before pushing.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread src/specify_cli/commands/init.py

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Copilot feedback. If not applicable, please explain why

jawwad-ali and others added 3 commits June 30, 2026 19:57
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>
… on empty stdin

The first version of this fix short-circuited on '_stdin_is_interactive()' (isatty) before typer.confirm, which broke 'init --here' when confirmation is piped (e.g. 'echo y | specify init --here' / CliRunner input='y\n') -- a non-TTY pipe with valid input was wrongly rejected, regressing test_init_here_without_force_preserves_shared_infra. Instead, call typer.confirm normally (piped 'y'/'n' is honored) and catch the Abort/EOFError it raises only when stdin is empty, converting that to the actionable '--force' guidance. This keeps the UX win for the no-input case without rejecting piped input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…warning

Address Copilot review on the --here non-empty path: (1) treat typer.Abort during an interactive confirm (e.g. Ctrl+C) as a normal cancellation (exit 0), and only emit the '--force' guidance + exit 1 when there is no TTY (empty stdin / EOF) -- no longer conflating the two; (2) move the 'will be merged / may overwrite' warning so it only shows when actually proceeding (force) or folded into the confirmation prompt, not on the fail-fast path where nothing is merged.

Piped confirmation (e.g. 'echo y | specify init --here') is still honored, which is why the prompt is attempted rather than refused outright when non-interactive -- the existing test_init_here_without_force_preserves_shared_infra pipes 'y' and must succeed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jawwad-ali jawwad-ali force-pushed the fix/init-here-noninteractive branch from b4a6fcd to dacd64b Compare June 30, 2026 14:59
@mnriem mnriem requested a review from Copilot June 30, 2026 15:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread src/specify_cli/commands/init.py Outdated
@mnriem

mnriem commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback. If not applicable, please explain why

Per Copilot review: do not call typer.confirm when stdin is not a TTY -- an open-but-idle non-TTY stdin (CI/agent) could block on the prompt. When the directory is non-empty and --force is not given, fail fast with '--force' guidance unless an interactive terminal is present. Interactive confirm still offers the merge-but-preserve path (distinct from --force, which overwrites); a Ctrl+C there is treated as a normal cancellation (exit 0). The merge/overwrite warning is only printed when actually proceeding, not on the fail-fast path.

Updated the preserve-merge E2E test to simulate an interactive terminal so it exercises the confirm path (non-interactive sessions now require --force).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

@mnriem addressed Copilot's feedback in the latest commit: init --here now fails fast (with --force guidance) when there's no interactive terminal, instead of prompting — so a non-TTY-but-open stdin can't block. The interactive confirm is kept for the merge-but-preserve path (distinct from --force overwrite). All --here tests pass; the prior pytest (ubuntu) failure is resolved.

@mnriem

mnriem commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Requesting changes — this introduces a breaking change to a legitimate, common usage.

The current final commit guards the confirmation with not _stdin_is_interactive() (isatty), which short-circuits before typer.confirm. That means echo y | specify init --here no longer works — piped confirmation is a standard scripting idiom, and this regresses it to an exit 1 that demands --force.

Critically, --force is not an equivalent substitute. Your own tests prove they differ:

  • no---force confirm path → preserves existing shared infra (e.g. a user-modified common.sh)
  • --force path → overwrites those files

So a script doing echo y | specify init --here previously got a non-destructive preserve-merge; after this PR it fails, and the suggested remedy (--force) does the opposite and clobbers customized files. There is now no non-interactive way to reach preserve-merge — that mode became interactive-only. The tell is that the existing test_init_here_without_force_preserves_shared_infra had to be monkeypatched (_stdin_is_interactive → True) to keep passing; a fix that requires neutering a passing test's environment is changing a real contract.

Your second revision (032e389ad) had the right shape. Please restore it: call typer.confirm normally so piped y/n is honored, and only convert the no-input case to the actionable error:

else:
    console.print(
        "[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]"
    )
    try:
        proceed = typer.confirm("Do you want to continue?")
    except (typer.Abort, EOFError):
        console.print(
            "[red]Error:[/red] Current directory is not empty and no confirmation "
            "input is available. Re-run with [bold]--force[/bold] to merge into it."
        )
        raise typer.Exit(1) from None
    if not proceed:
        console.print("[yellow]Operation cancelled[/yellow]")
        raise typer.Exit(0)

This keeps all cases working: piped y → preserve-merge; piped n → cancel (exit 0); closed/empty stdin (< /dev/null) → EOF/Abort → actionable --force error (exit 1); interactive TTY → unchanged.

The only uncovered case is an open-but-idle non-TTY pipe (the original blocking bug). Breaking the common echo y idiom to fix that rare edge case is the wrong trade — if it matters, handle it with an explicit --yes/--no-input flag or a read timeout, not by dropping piped-confirmation support.

Per maintainer review: restore the second-revision shape. Calling
typer.confirm normally keeps 'echo y | specify init --here' reaching the
non-destructive preserve-merge path (and piped 'n' cancels with exit 0).
Only when no confirmation input is available at all (closed/empty stdin
-> typer.Abort/EOFError) is it converted into the actionable error that
points at --force. This drops the _stdin_is_interactive fail-fast that
broke the common piped-confirm idiom and made preserve-merge
interactive-only. The preserve test no longer needs to monkeypatch
_stdin_is_interactive - it passes on the real contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

Thanks @mnriem — you're right, and I've restored the second-revision shape in de42535.

typer.confirm is now called normally, so:

  • piped y → non-destructive preserve-merge (the common echo y | specify init --here idiom works again)
  • piped n → cancel, exit 0
  • closed/empty stdin (< /dev/null) → Abort/EOFError → actionable --force error, exit 1
  • interactive TTY → unchanged

I dropped the _stdin_is_interactive() fail-fast that had made preserve-merge interactive-only, and test_init_here_without_force_preserves_shared_infra no longer monkeypatches _stdin_is_interactive — it passes on the real contract now, which was exactly the tell you flagged. The open-but-idle non-TTY pipe is left as the one uncovered edge case, to be handled with an explicit --yes/--no-input flag or a read timeout if it ever matters, rather than by breaking piped confirmation.

(AI-assisted: implemented with Claude Code under my direction and review.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants