mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
* feat: thin-client upgrade prompt core (orchestrator + helpers) Adds the maybePromptForUpgrade orchestrator with lockfile gating, atomic state-file IO, per-entry shape validation, decision matrix, D5 binary-advance verifier, prompt-scoped SIGINT handler, and DI seams for tests. Sibling helper promptLineStderr in cli-util.ts resolves to null on stdin EOF or after a 5min timeout instead of hanging. 50 unit tests, all green. Not wired into the CLI yet — that's the next commit. * feat: wire thin-client upgrade prompt into the identity banner printIdentityBannerBestEffort calls maybePromptForUpgrade after the banner prints (both cache hit and cache miss paths). bannerSuppressed + BrainIdentity are now exported for the orchestrator's consumption. bannerSuppressed early return guarantees bannerIsSuppressed=false at the call site. * feat: gbrain remote doctor — thin_client_upgrade_drift check Surfaces remote-version drift in non-TTY/quiet/CI contexts where the interactive prompt is suppressed. Returns ok+inconclusive on network error (informational; mcp_smoke covers the genuinely-down case with fail). Returns ok on local>=remote or patch drift; warn on minor/major drift with a fix hint pointing at gbrain upgrade, or the manual install URL if state shows a prior failed attempt. Test fixture now dispatches JSON-RPC tools/call by tool name so runUpgradeDriftCheck can exercise the full happy + prior_failed + stale-version paths against a real-shape MCP response. * chore: bump version and changelog (v0.31.11) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
/**
|
|
* Prompt on stdout, read one line from stdin, return trimmed string.
|
|
* Shared helper used by interactive CLI flows (init, apply-migrations, etc.).
|
|
*/
|
|
export function promptLine(prompt: string): Promise<string> {
|
|
return new Promise((resolve) => {
|
|
process.stdout.write(prompt);
|
|
process.stdin.setEncoding('utf-8');
|
|
process.stdin.once('data', (chunk) => {
|
|
const data = chunk.toString().trim();
|
|
process.stdin.pause();
|
|
resolve(data);
|
|
});
|
|
process.stdin.resume();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Same as promptLine, but writes the prompt to stderr instead of stdout AND
|
|
* resolves to `null` on stdin EOF or after `timeoutMs` (default 5 minutes)
|
|
* instead of hanging forever. Used when the surrounding command must keep
|
|
* stdout clean for machine-readable output (JSON, piped data); the thin-client
|
|
* upgrade prompt fires before any routed command runs and would otherwise
|
|
* pollute `gbrain query > out.json`.
|
|
*
|
|
* Return contract:
|
|
* - resolves to a `string` (trimmed) on the first stdin data event
|
|
* - resolves to `null` on stdin 'end' (parent shell closed, /dev/null piped
|
|
* past the TTY check, etc.) or after `timeoutMs` elapses
|
|
* - never rejects
|
|
*
|
|
* Callers MUST handle null explicitly — it is NOT the same as an empty string
|
|
* (which means "user pressed Enter").
|
|
*/
|
|
export function promptLineStderr(prompt: string, opts: { timeoutMs?: number } = {}): Promise<string | null> {
|
|
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
return new Promise((resolve) => {
|
|
process.stderr.write(prompt);
|
|
process.stdin.setEncoding('utf-8');
|
|
let settled = false;
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
const cleanup = () => {
|
|
process.stdin.removeListener('data', onData);
|
|
process.stdin.removeListener('end', onEnd);
|
|
if (timer !== null) clearTimeout(timer);
|
|
process.stdin.pause();
|
|
};
|
|
const onData = (chunk: Buffer | string) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
cleanup();
|
|
resolve(chunk.toString().trim());
|
|
};
|
|
const onEnd = () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
cleanup();
|
|
resolve(null);
|
|
};
|
|
if (timeoutMs > 0) {
|
|
timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
cleanup();
|
|
resolve(null);
|
|
}, timeoutMs);
|
|
}
|
|
process.stdin.once('data', onData);
|
|
process.stdin.once('end', onEnd);
|
|
process.stdin.resume();
|
|
});
|
|
}
|