Skip to content

Commit 945743b

Browse files
committed
fix(cli): gate the --continue-without-plan confirm on the TTY check
The flag-consent path was the one prompt on `stash impl` that never went through `isInteractive()`. With no plan on disk and `--continue-without-plan` passed, a CI runner still reached `p.confirm`, which reads /dev/tty — the same hang the rest of this branch closes. Worse if the prompt did resolve: the confirm is `initialValue: false`, so a non-interactive run would cancel work the flag had explicitly authorised. The flag is the consent. Take it as given when non-interactive; keep the interactive confirmation unchanged.
1 parent bb0a90f commit 945743b

3 files changed

Lines changed: 31 additions & 3 deletions

File tree

.changeset/hungry-spoons-shake.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Fix `stash impl` and `stash init` hanging on CI runners that allocate a TTY.
66

7-
Three prompts decided whether to run interactively without going through the
7+
Four prompts decided whether to run interactively without going through the
88
shared TTY helper, so on a CI runner with an allocated TTY they rendered a clack
99
prompt and blocked forever on `/dev/tty` — a silent hang with no error and no
1010
timeout:
@@ -16,8 +16,12 @@ timeout:
1616
question, gated on `process.stdout.isTTY` and did not consult `CI` at all —
1717
so they hung on any CI runner with a TTY, whatever the spelling. Gating on
1818
stdout was also the wrong stream: a redirected stdin still hangs a prompt.
19+
- `stash impl --continue-without-plan` confirmed the flag with a second prompt
20+
that was not gated at all, so a CI run with no plan on disk blocked there even
21+
though the flag had already granted consent. The flag is now taken as consent
22+
in non-interactive runs and only re-confirmed interactively.
1923

20-
All three now use the shared `isInteractive()` helper (stdin is a TTY and `CI`
24+
All four now use the shared `isInteractive()` helper (stdin is a TTY and `CI`
2125
is not set to `1`/`true` in any case), matching `stash plan`. Non-interactive
2226
runs take the path they always should have: `stash init` skips the chain offer
2327
and prints the `plan --target` hint, the Proxy-vs-SDK question defaults to

packages/cli/src/commands/impl/__tests__/impl.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ describe('implCommand — CI detection with a TTY attached', () => {
143143
})
144144
}
145145

146+
it('does not re-confirm --continue-without-plan under CI when no plan exists', async () => {
147+
// The flag is the consent. Prompting again under CI blocks on /dev/tty,
148+
// and the confirm is default-no, so a resolved prompt would cancel a run
149+
// the user explicitly asked for.
150+
vi.stubEnv('CI', '1')
151+
fs.rmSync(path.join(tmpDir, '.cipherstash', 'plan.md'))
152+
const runSpy = vi
153+
.spyOn(howToProceedStep, 'run')
154+
.mockResolvedValue({} as never)
155+
156+
await expect(
157+
implCommand({ 'continue-without-plan': true }, {}),
158+
).resolves.toBeUndefined()
159+
160+
expect(confirmMock).not.toHaveBeenCalled()
161+
expect(runSpy).not.toHaveBeenCalled()
162+
})
163+
146164
it('is interactive when CI is unset and stdin is a TTY', async () => {
147165
vi.stubEnv('CI', '')
148166
const runSpy = vi

packages/cli/src/commands/impl/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,13 @@ export async function implCommand(
230230
} else {
231231
// No plan on disk. Branch on flag / TTY / interactive.
232232
if (continueWithoutPlan) {
233-
await confirmContinueWithoutPlan()
233+
// The flag *is* the consent. Re-prompting for it non-interactively
234+
// is the same hang this gate exists to close (clack `confirm` reads
235+
// /dev/tty), and `confirmContinueWithoutPlan` is default-no, so a
236+
// prompt that did resolve would cancel a run the user asked for.
237+
if (isTTY) {
238+
await confirmContinueWithoutPlan()
239+
}
234240
} else if (!isTTY) {
235241
p.log.error(
236242
`No plan at \`${PLAN_REL_PATH}\`. Run \`${cli} plan\` first, or pass --continue-without-plan to skip planning.`,

0 commit comments

Comments
 (0)