54 lines
3.2 KiB
TypeScript
54 lines
3.2 KiB
TypeScript
// Bypass mode: the owner's opt-in to let an AI agent press the owner-only buttons too (Submit for review, releases and
|
|
// rollouts, testers, review replies, prices, deletes, cancels, console final buttons). Default OFF. Stored per machine
|
|
// in ~/.storebound/settings.json, never in a project's config or repo:
|
|
// storebound settings bypass on | off | status
|
|
// When ON: the MCP server lists `owner_step` (runs one owner command, dry run first), `storebound api` sends owner-only
|
|
// operations with --yes --confirm <operationId>, console plans tell the agent it may press buttons.final. Every owner
|
|
// action an agent runs is appended to <project>/.storebound/owner-actions.log. `bypass off` restores the old behaviour
|
|
// at once. An AI client may still ask the owner itself for legal agreements, passwords and 2FA (its own rules).
|
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
export interface Settings { bypass?: { on: boolean; since: string } }
|
|
|
|
export const settingsFile = (env: Record<string, string | undefined> = process.env): string =>
|
|
join(env.STOREBOUND_HOME ?? join(homedir(), '.storebound'), 'settings.json');
|
|
|
|
export function readSettings(env: Record<string, string | undefined> = process.env): Settings {
|
|
try { return JSON.parse(readFileSync(settingsFile(env), 'utf8')) as Settings; } catch { return {}; }
|
|
}
|
|
|
|
export function writeSettings(s: Settings, env: Record<string, string | undefined> = process.env): void {
|
|
const f = settingsFile(env);
|
|
mkdirSync(dirname(f), { recursive: true });
|
|
writeFileSync(f, `${JSON.stringify(s, null, 2)}\n`);
|
|
}
|
|
|
|
/** Whether the owner turned bypass mode on for this machine. */
|
|
export const bypassOn = (env: Record<string, string | undefined> = process.env): boolean => readSettings(env).bypass?.on === true;
|
|
|
|
export function setBypass(on: boolean, env: Record<string, string | undefined> = process.env): Settings {
|
|
const s = readSettings(env);
|
|
s.bypass = { on, since: new Date().toISOString() };
|
|
writeSettings(s, env);
|
|
return s;
|
|
}
|
|
|
|
/** Owner-only commands `owner_step` may run in bypass mode (CLI words). Everything else has its own MCP tool. */
|
|
export const OWNER_COMMANDS = [
|
|
'apple submit', 'apple resubmit', 'apple iap-submit', 'apple release', 'apple preorder',
|
|
'play release', 'play rollout',
|
|
'reviews', 'apple testflight', 'play products', 'play subscriptions', 'apple iap', 'apple subscriptions', 'prices',
|
|
] as const;
|
|
export type OwnerCommand = typeof OWNER_COMMANDS[number];
|
|
|
|
export const BYPASS_NOTICE = 'bypass mode is ON (storebound settings bypass off turns it off): the agent runs owner steps itself; each one is logged to .storebound/owner-actions.log';
|
|
|
|
/** One line per owner action an agent ran: when, what, the result. */
|
|
export function logOwnerAction(root: string, entry: { command: string; dry: boolean; exit: number; summary?: string }): string {
|
|
const f = join(root, '.storebound', 'owner-actions.log');
|
|
if (!existsSync(dirname(f))) mkdirSync(dirname(f), { recursive: true });
|
|
appendFileSync(f, `${new Date().toISOString()}\t${entry.dry ? 'dry' : 'RAN'}\texit ${entry.exit}\t${entry.command}${entry.summary ? `\t${entry.summary.replace(/\s+/g, ' ').slice(0, 300)}` : ''}\n`);
|
|
return f;
|
|
}
|