Skip to content

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

Merged
mnriem merged 6 commits into
github:mainfrom
jawwad-ali:fix/init-here-noninteractive
Jul 13, 2026
Merged

fix(init): don't block on confirmation for 'init --here' without a TTY#3236
mnriem merged 6 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 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
@mnriem

mnriem commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

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

@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.

@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.)

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

jawwad-ali and others added 5 commits July 10, 2026 21:44
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>
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>
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 jawwad-ali force-pushed the fix/init-here-noninteractive branch from de42535 to fc5c69a Compare July 10, 2026 17:24
@mnriem mnriem requested a review from Copilot July 13, 2026 12:40

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: 2
  • Review effort level: Medium

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

mnriem commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

…o 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>

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: 0 new
  • Review effort level: Medium

@mnriem mnriem self-requested a review July 13, 2026 20:28
@mnriem mnriem merged commit 9930834 into github:main Jul 13, 2026
12 checks passed
@mnriem

mnriem commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

davidebibm pushed a commit to davidebibm/spec-kit that referenced this pull request Jul 14, 2026
github#3236)

* 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>

* fix(init): honor piped confirmation for 'init --here'; only fail-fast 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>

* fix(init): distinguish interactive cancel from no-input; defer merge 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>

* fix(init): fail fast on non-interactive --here instead of prompting

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>

* fix(init): honor piped y/n for 'init --here', error only on no-input

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>

* 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>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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