apple signing status|ensure: bundle id capabilities, a distribution certificate from a key made locally (kept in .storebound/signing, never printed), App Store profiles installed for Xcode; Developer ID 403 explained. apple build / apple upload: xcodebuild archive + export with a generated ExportOptions.plist, next build number, App Store Connect API build upload (or altool), processing wait. Native, Capacitor, React Native, Flutter, Unity projects. play build / play upload: Gradle bundleRelease with the upload key passed through the environment, next versionCode read back from the AAB, testing tracks only. originality: offline Guideline 4.3 check (name, listing text, icon and screenshot perceptual hashes, project structure). New skill build-and-sign; zero new dependencies.
83 lines
4.1 KiB
TypeScript
83 lines
4.1 KiB
TypeScript
// Runs one storebound command as a child process: the shared core of `storebound mcp` and `storebound serve --stdio`.
|
|
// Works from source (`bun cli/index.ts ...`) and from a `bun build --compile` binary (the desktop sidecar), where the
|
|
// binary re-executes itself. Output is streamed line by line (progress) and the `--json` result line is picked out.
|
|
import { join } from 'node:path';
|
|
import { RESULT_MARK } from './log';
|
|
import { toolPath } from './tools';
|
|
|
|
/** True inside a `bun build --compile` executable (the entry module lives in Bun's embedded file system). */
|
|
export const compiled = (): boolean => /^(\/\$bunfs\/|[A-Z]:[\\/]~BUN[\\/])/.test(Bun.main);
|
|
|
|
/** Command prefix that starts this storebound: the compiled binary itself, or bun + cli/index.ts. */
|
|
export function selfCommand(): string[] {
|
|
return compiled() ? [process.execPath] : [process.execPath, join(import.meta.dir, '..', 'index.ts')];
|
|
}
|
|
|
|
export interface RunOptions {
|
|
cwd: string;
|
|
/** Extra environment for this call only (credentials from the desktop keychain, never written anywhere). */
|
|
env?: Record<string, string>;
|
|
/** Every output line as it arrives (stdout and stderr), without the machine result line. */
|
|
onLine?: (line: string, stream: 'stdout' | 'stderr') => void;
|
|
/** Ask for the marked `--json` result line (STOREBOUND_MACHINE=1). */
|
|
machine?: boolean;
|
|
}
|
|
|
|
export interface RunResult { code: number; out: string; data?: unknown; cancelled: boolean }
|
|
|
|
export interface Running { result: Promise<RunResult>; cancel(): void }
|
|
|
|
/** Env names a caller may pass per call; anything else in `env` is ignored. */
|
|
export const CALL_ENV = ['PLAY_SERVICE_ACCOUNT', 'ASC_KEY_ID', 'ASC_ISSUER_ID', 'ASC_P8'] as const;
|
|
|
|
export function pickCallEnv(env: Record<string, unknown> | undefined): Record<string, string> {
|
|
const out: Record<string, string> = {};
|
|
for (const k of CALL_ENV) { const v = env?.[k]; if (typeof v === 'string' && v) out[k] = v; }
|
|
return out;
|
|
}
|
|
|
|
/** Environment of a child: the parent's minus anything secret to the session, plus the per-call values. */
|
|
export function childEnv(extra: Record<string, string> = {}, machine = false): Record<string, string> {
|
|
const base: Record<string, string> = {};
|
|
// never inherited: the session secret, and the owner's confirmation (serve passes it per call, after a verified token)
|
|
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !k.startsWith('STOREBOUND_SESSION') && k !== 'STOREBOUND_OWNER_CONFIRMED') base[k] = v;
|
|
// app-managed tools (desktop data folder) come first on PATH
|
|
base.PATH = toolPath(base);
|
|
return { ...base, ...extra, NO_COLOR: '1', ...(machine ? { STOREBOUND_MACHINE: '1', STOREBOUND_SERVE: '1' } : {}) };
|
|
}
|
|
|
|
export async function pump(stream: ReadableStream<Uint8Array>, kind: 'stdout' | 'stderr', each: (line: string, kind: 'stdout' | 'stderr') => void): Promise<void> {
|
|
const dec = new TextDecoder();
|
|
let buf = '';
|
|
const reader = stream.getReader();
|
|
for (;;) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buf += dec.decode(value, { stream: true });
|
|
let nl: number;
|
|
while ((nl = buf.indexOf('\n')) >= 0) { each(buf.slice(0, nl).replace(/\r$/, ''), kind); buf = buf.slice(nl + 1); }
|
|
}
|
|
buf += dec.decode();
|
|
if (buf) each(buf, kind);
|
|
}
|
|
|
|
export function runCommand(argv: string[], o: RunOptions): Running {
|
|
const p = Bun.spawn([...selfCommand(), ...argv], { cwd: o.cwd, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', env: childEnv(o.env, o.machine) });
|
|
let cancelled = false;
|
|
const lines: string[] = [];
|
|
let data: unknown;
|
|
const each = (line: string, kind: 'stdout' | 'stderr'): void => {
|
|
if (kind === 'stdout' && line.startsWith(RESULT_MARK)) {
|
|
try { data = JSON.parse(line.slice(RESULT_MARK.length)); } catch { lines.push(line); }
|
|
return;
|
|
}
|
|
lines.push(line);
|
|
o.onLine?.(line, kind);
|
|
};
|
|
const result = (async (): Promise<RunResult> => {
|
|
await Promise.all([pump(p.stdout, 'stdout', each), pump(p.stderr, 'stderr', each)]);
|
|
const code = await p.exited;
|
|
return { code, out: lines.join('\n').trim(), ...(data === undefined ? {} : { data }), cancelled };
|
|
})();
|
|
return { result, cancel: () => { cancelled = true; p.kill(); } };
|
|
}
|