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(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 { const m = new Map(); 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 { 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: " 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 { 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}`); process.exitCode = DECLINED_EXIT; return false; }