Google API errors as typed findings, never raw JSON: cli/lib/google-errors.ts parses SERVICE_DISABLED (api-disabled: service, Cloud project, console page), PERMISSION_DENIED (no-app-access: which app, Users and permissions), 401 / invalid_grant (auth-revoked) and 429 (quota); health / analytics print one line with the URL, raw text in detail and the log; desktop Health tab: the line, 'Turn it on in Google Cloud' and 'Check again', raw text in a collapsed Details; Portfolio shows a Cloud project blocking several apps once. Findings exit codes are results: the sidecar reports outcome (ok / findings / declined / failed) and the command's own error line; scan / check / validate / health exit 1 with findings show and cache the findings; no bare 'Exit code N' anywhere. tests/google-errors.test.ts, boot check errors cases (api-disabled, scan-findings, scan-failed) (pitfall 57)

This commit is contained in:
CRTkafa 2026-10-13 23:20:04 +08:00
parent f69c10d3a0
commit 89256f1e80
28 changed files with 666 additions and 47 deletions

View file

@ -20,7 +20,7 @@ import pkg from '../package.json' with { type: 'json' };
import { canonical, parseSecret, TokenGuard } from './lib/confirm-token';
import { CONFIG_NAMES } from './lib/config';
import {
DESKTOP_PROTOCOL, ERR, type AppStatus, type MethodDescriptor, type PortfolioResult, type ProjectSummary, type ToolResult, type VirtualProject,
DESKTOP_PROTOCOL, ERR, type AppStatus, type MethodDescriptor, type PortfolioResult, type ProjectSummary, type ToolResult, type VirtualProject, errorLine, outcomeOf,
} from './lib/desktop-protocol';
import { emptyStatus, fillAppStatus, lastLine } from './lib/app-status';
import { RENDER_FILE_RE } from './lib/project-files';
@ -441,9 +441,11 @@ export class Session {
const r = this.track(id, this.run(argv, { cwd: project, env, machine: true, onLine: (line, stream) => this.progress(id, { line, stream }) }));
const res = await r.result;
if (res.cancelled || this.cancelled.has(id)) throw new RpcError(ERR.cancelled, 'Request cancelled');
const outcome = outcomeOf(t.write ? '' : t.name, res.code, res.data !== undefined);
const error = outcome === 'failed' ? errorLine(res.err ?? '', res.out) : '';
return {
method, ok: res.code === 0, exitCode: res.code, dryRun: t.write ? dryRun : false, command: `storebound ${argv.join(' ')}`, output: cut(res.out),
...(res.data === undefined ? {} : { data: res.data }),
...(res.data === undefined ? {} : { data: res.data }), outcome, ...(error ? { error } : {}),
};
}
@ -461,7 +463,7 @@ export class Session {
const argv = [...cmd, '--store', play ? 'play' : 'apple', ...(args.icons === true ? ['--icons'] : []), '--json'];
const r = await this.track(id, this.run(argv, { cwd: neutralDir(), env, machine: true, onLine: (line, stream) => this.progress(id, { line, stream }) })).result;
if (r.cancelled || this.cancelled.has(id)) throw new RpcError(ERR.cancelled, 'Request cancelled');
if (r.data === undefined) throw new RpcError(ERR.internal, lastLine(r.out) || `exit ${r.code}`);
if (r.data === undefined) throw new RpcError(ERR.internal, errorLine(r.err ?? '', r.out) || `storebound ${argv[0]} ${argv[1] ?? ''} stopped without an error message (exit ${r.code})`);
return r.data;
}