flow run: a declined confirmation stops the flow; --apply --yes confirms write steps

confirm() prints 'declined: <reason>' and exits 3 whenever the answer is no or nobody can answer (not a
terminal without --yes, serve without the owner's dialog), so no command reports success after declining.
flow run stops at such a step with 'step N declined: <reason>' (status declined), never ok. flow run
--apply --yes passes --yes to write steps only; --yes without --apply is refused. Pitfall 24.
This commit is contained in:
CRTkafa 2026-10-12 08:44:56 +08:00
parent 5fbb9e63e3
commit 366362d62b
12 changed files with 116 additions and 19 deletions

View file

@ -27,15 +27,33 @@ export function byAppleLocale(langs: Language[]): Map<string, Language> {
* so the answer is no. The one exception: a desktop owner action (serve.ts OWNER_TOOLS: rollout changes, the phased
* release, sending one approved reply) whose confirm token the sidecar verified after the owner pressed the button of
* a native dialog showing the dry run; serve sets STOREBOUND_OWNER_CONFIRMED for that one child only.
* A "no" (answered, or nobody could answer) is never a success: it prints "declined: <reason>" on stderr and sets the
* exit code to DECLINED_EXIT, so a script or `flow run` stops there instead of going on as if the write happened.
*/
export async function confirm(ctx: Ctx, question: string): Promise<boolean> {
if (process.env.STOREBOUND_SERVE === '1') {
if (process.env.STOREBOUND_OWNER_CONFIRMED === '1') { console.error(`confirmed by the owner in the desktop dialog: ${question}`); return true; }
console.error(`confirm-required: ${question}\n terminal only: run it in a terminal`); return false;
console.error(`confirm-required: ${question}\n terminal only: run it in a terminal`);
return declined('confirm-required: terminal only');
}
if (ctx.args.has('yes')) return true;
if (!process.stdin.isTTY) { console.log(`${question}\n not a terminal: re-run with --yes to confirm`); return false; }
if (!process.stdin.isTTY) {
console.log(`${question}\n not a terminal: re-run with --yes to confirm`);
return declined('not a terminal and no --yes (re-run with --yes to confirm)');
}
process.stdout.write(`${question} [y/N] `);
for await (const line of console) return /^y(es)?$/i.test(line.trim());
for await (const line of console) return /^y(es)?$/i.test(line.trim()) || declined('the owner answered no');
return declined('no answer (stdin closed)');
}
/** Exit code of a command whose confirmation was declined or could not be asked. */
export const DECLINED_EXIT = 3;
/** Prefix of the stderr line a declined confirmation prints (`flow run` reports it as "step N declined: ..."). */
export const DECLINED_MARK = 'declined: ';
/** A declined confirmation: the reason on stderr, a nonzero exit code; returns false for confirm(). */
export function declined(reason: string): false {
console.error(`${DECLINED_MARK}${reason}`);
process.exitCode = DECLINED_EXIT;
return false;
}