65 lines
3.3 KiB
TypeScript
65 lines
3.3 KiB
TypeScript
import type { Args } from './args';
|
|
import type { Language, Loaded } from './config';
|
|
import { playCodes } from './config';
|
|
import * as rt from './rt';
|
|
import { stdinIsTTY } from './tty';
|
|
import { promptOut } from './log';
|
|
|
|
export interface Ctx { loaded: Loaded; args: Args }
|
|
|
|
export function need<T>(v: T | undefined, what: string): T {
|
|
if (v === undefined || v === null) throw new Error(`storebound.config.ts: ${what} is not set`);
|
|
return v;
|
|
}
|
|
|
|
/** Play language code -> language entry. */
|
|
export function byPlayCode(langs: Language[]): Map<string, Language> {
|
|
const m = new Map<string, Language>();
|
|
for (const l of langs) for (const c of playCodes(l)) m.set(c, l);
|
|
return m;
|
|
}
|
|
|
|
/** App Store locale -> language entry. */
|
|
export function byAppleLocale(langs: Language[]): Map<string, Language> {
|
|
return new Map(langs.filter(l => l.apple).map(l => [l.apple!, l] as const));
|
|
}
|
|
|
|
/**
|
|
* Asks on the terminal unless --yes. Store actions that are public or irreversible go through this.
|
|
* Under `storebound serve` (the desktop sidecar) nothing can answer and nothing may skip the question, not even --yes,
|
|
* 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 declined('confirm-required: terminal only');
|
|
}
|
|
if (ctx.args.has('yes')) return true;
|
|
if (!stdinIsTTY()) {
|
|
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)');
|
|
}
|
|
promptOut().write(`${question} [y/N] `);
|
|
const line = await rt.readLine();
|
|
return line !== null && /^y(es)?$/i.test(line.trim()) || declined(line === null ? 'no answer (stdin closed)' : 'the owner answered no');
|
|
}
|
|
|
|
/** 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}`);
|
|
// a ship step (cli/lib/ship-step.ts) reports the reason in error.message of its exit 3
|
|
const ship = (globalThis as unknown as Record<symbol, { declined?: string } | null | undefined>)[Symbol.for('storebound.shipRun')];
|
|
if (ship) ship.declined = reason;
|
|
process.exitCode = DECLINED_EXIT;
|
|
return false;
|
|
}
|