Files
gbrain/src/core/cli-util.ts
T
0410dc4b42 v0.31.11 feat: thin-client auto-upgrade prompt (notice + act on remote bumps) (#816)
* 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>
2026-05-10 18:59:11 -07:00

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();
});
}