You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(cli): make plan --complete-rollout automatable with --yes + honest exit (rc.2 M2) (#686)
* feat(cli): make `plan --complete-rollout` automatable with --yes + honest exit (rc.2 M2)
`--complete-rollout` skips the production-deploy gate, so it requires explicit
consent. Previously that was an interactive p.confirm with no bypass: a
non-interactive run auto-cancelled (default-no) and exited 0 via CancelledError
— so CI/agents got exit 0 but no plan was drafted, and would assume one existed.
- New `--yes` flag confirms the gate-skip without a prompt (automation path).
- Non-interactive `--complete-rollout` WITHOUT `--yes` now refuses with a
non-zero exit (CliExit(1)) and points at --yes, instead of silently
cancelling with exit 0. An interactive decline stays exit 0 (a deliberate
'no' is not a failure).
- isInteractive is computed once up front (the confirm needs it too).
- Registry + help + `stash-cli` skill updated; messages.plan.* leaders added
for e2e stability.
Tests: 2 pty-less e2e (refuses+exit-1 without --yes; --yes confirms + proceeds),
plus manifest resolves the new flag. Suite 540 unit / 64 e2e green, code:check
clean. Changeset: stash minor.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
* refactor(cli): use shared isInteractive() in plan; drop dead cli param
Review follow-up on the M2 complete-rollout change (finding #1 + #3; also
Copilot's sole inline comment):
- plan's interactivity gate re-inlined `process.stdin.isTTY && CI !== 'true'`,
which only matches exact lowercase `true` — it misses `CI=1`/`CI=TRUE` and so
diverged from the shared `isInteractive()` (config/tty.ts, whose `isCiEnv()`
matches `/^(1|true)$/i`) and from the case-insensitive contract the stash-cli
skill documents. Swap the inline for `isInteractiveTty()` (compute-once
preserved) so a CI=1 runner with a TTY takes the non-interactive path.
- Drop the unused `cli` parameter from `confirmCompleteRollout`'s opts (the
refusal hardcodes the `--yes` hint; `cli` was threaded but never read).
No behaviour change on the documented CI=true / TTY paths. plan-complete-rollout
e2e 2/2 and plan unit 22/22 green; build + biome clean.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
'Plan the entire encryption lifecycle (schema-add through drop) in one document. Skips the production-deploy gate; only safe when this database is not backing a deployed application.',
161
+
'Plan the entire encryption lifecycle (schema-add through drop) in one document. Skips the production-deploy gate; only safe when this database is not backing a deployed application. Needs confirmation — an interactive prompt, or --yes non-interactively (else it exits non-zero without drafting).',
162
+
},
163
+
{
164
+
name: '--yes',
165
+
description:
166
+
"Confirm --complete-rollout's gate-skip without a prompt (for automation / CI). No effect without --complete-rollout.",
'`--complete-rollout` plans the full encryption lifecycle (schema-add through drop) in one document. It SKIPS the production-deploy gate that protects backfill from running before dual-writes are live.',
54
65
)
55
66
p.log.warn(
56
67
'Only safe when this database is not backing a deployed application — local development, ephemeral test environments, or freshly seeded sandboxes. If a deployed app writes to this database, rows inserted during the planned backfill will land in plaintext only and you will need a recovery pass.',
57
68
)
69
+
70
+
if(opts.assumeYes){
71
+
p.log.info(
72
+
`${messages.plan.completeRolloutConfirmed} by your explicit confirmation.`,
73
+
)
74
+
return
75
+
}
76
+
77
+
if(!opts.isInteractive){
78
+
p.log.error(
79
+
`${messages.plan.completeRolloutNeedsYes}. Re-run with \`--yes\` to confirm non-interactively — only safe when no deployed application writes to this database (see the warnings above).`,
80
+
)
81
+
// Non-zero: the requested plan was NOT drafted. Distinct from a user's
82
+
// interactive decline (a deliberate "no" → exit 0 via CancelledError).
83
+
thrownewCliExit(1)
84
+
}
85
+
58
86
constok=awaitp.confirm({
59
87
message: 'Proceed with a complete-rollout plan?',
60
88
initialValue: false,
@@ -124,8 +152,12 @@ async function detectPlanStep(cwd: string): Promise<PlanStep> {
124
152
* Flags:
125
153
* `--complete-rollout` — escape hatch for databases without a deployed
126
154
* application. Plans schema-add through drop in
127
-
* one document with no deploy gate. Confirms
128
-
* (default-no) before generating.
155
+
* one document with no deploy gate. Needs explicit
156
+
* confirmation: an interactive default-no prompt, or
157
+
* `--yes` non-interactively (else it refuses with a
158
+
* non-zero exit rather than silently doing nothing).
159
+
* `--yes` — confirm `--complete-rollout`'s gate-skip without a
160
+
* prompt, for automation. No effect without it.
129
161
*/
130
162
exportasyncfunctionplanCommand(
131
163
flags: Record<string,boolean>={},
@@ -154,6 +186,15 @@ export async function planCommand(
154
186
155
187
p.intro('CipherStash Plan')
156
188
189
+
// Interactive only when stdin is a real TTY and we're not in CI — via the
190
+
// shared `isInteractive()` (config/tty.ts) so this gate stays identical to
191
+
// every other prompt gate (its `isCiEnv()` treats `CI=1`/`CI=TRUE` as CI too,
192
+
// which a bare `CI !== 'true'` inline would miss). Keying off
193
+
// `process.stdout.isTTY` alone is wrong: a redirected stdin still hangs the
194
+
// agent-target picker (clack `select` reads from /dev/tty). Computed up here
195
+
// because the complete-rollout confirmation needs it too.
196
+
constisInteractive=isInteractiveTty()
197
+
157
198
try{
158
199
if(existsSync(resolve(cwd,PLAN_REL_PATH))){
159
200
p.log.warn(
@@ -163,7 +204,10 @@ export async function planCommand(
163
204
164
205
letplanStep: PlanStep
165
206
if(flags['complete-rollout']){
166
-
awaitconfirmCompleteRollout()
207
+
awaitconfirmCompleteRollout({
208
+
assumeYes: flags.yes??false,
209
+
isInteractive,
210
+
})
167
211
planStep='complete'
168
212
}else{
169
213
planStep=awaitdetectPlanStep(cwd)
@@ -181,13 +225,6 @@ export async function planCommand(
Copy file name to clipboardExpand all lines: skills/stash-cli/SKILL.md
+4-3Lines changed: 4 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -123,7 +123,7 @@ If a command fails on authentication, re-run `stash auth login`. Do not inspect
123
123
124
124
A command is interactive only when **stdin is a TTY and `CI` is not set** to `1`/`true` (case-insensitive). Otherwise prompts are skipped.
125
125
126
-
There is **no global `--non-interactive`, `--yes`, or `--json` flag.** Each command carries its own escape hatch:
126
+
There is **no global `--non-interactive` or `--json` flag** (and no global `--yes` — but a few commands carry a scoped one, e.g. `plan --complete-rollout --yes`). Each command carries its own escape hatch:
stash plan --complete-rollout # interactive: default-no confirm
242
+
stash plan --complete-rollout --yes --target claude-code # non-interactive / CI
242
243
stash plan --target claude-code
243
244
```
244
245
@@ -250,7 +251,7 @@ Pre-flights `.cipherstash/context.json` (errors with "Run `stash init` first" if
250
251
|---|---|
251
252
| No `dual_writing` event recorded |**Encryption rollout** — schema-add + dual-write code. Ends at the deploy gate. |
252
253
| A column has `dual_writing` or later |**Encryption cutover** — backfill, schema rename, read-path switch, drop. Requires the rollout to be deployed. |
253
-
|`--complete-rollout` passed |**Complete rollout** — schema-add through drop, no deploy gate. Default-no confirm with a loud warning. |
254
+
|`--complete-rollout` passed |**Complete rollout** — schema-add through drop, no deploy gate. Needs consent: an interactive default-no confirm, or `--yes` non-interactively (without it, a non-interactive run **exits non-zero without drafting** rather than silently doing nothing). |
254
255
255
256
The agent writes a machine-readable header into the plan:
0 commit comments