From ed11fdd58c928075e4b8d385d3a274f6a9eb00d2 Mon Sep 17 00:00:00 2001 From: Brandon Lipman Date: Fri, 8 May 2026 20:21:50 -0400 Subject: [PATCH] fix(init): --help should not mutate config or scan filesystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gbrain init --help` (and `-h`) currently fall through to the smart-detection branch in runInit(), which scans cwd for .md files and on a directory with 1000+ files prints "Found ~1500 .md files. For a brain this size, Supabase gives faster search..." then defaults to PGLite — calling saveConfig() and overwriting any existing Postgres config with `engine: 'pglite' + database_path: ~/.gbrain/brain.pglite`. Confirmed in the wild: ran `gbrain init --help` from $HOME on a machine where ~/.gbrain/config.json pointed at a Supabase Postgres brain with 10K+ pages. The config was silently flipped to PGLite. The Supabase data was intact, but gbrain stopped pointing at it until the config was manually restored. Root cause: cli.ts:62-69 only routes --help → printOpHelp() for shared-op commands; CLI_ONLY commands (init, embed, etc.) fall through to their handler with --help still in argv. None of them check for it. Fix: add a --help/-h guard at the top of runInit() that prints help text and returns. Help should never mutate state — Postel's robustness principle for CLI tools. Help text covers all flags (engine selection, AI provider options, thin-client mode) so users running `--help` get the canonical list rather than having to read the source. A wider architectural fix — adding --help routing for all CLI_ONLY commands in cli.ts — is plausible follow-up, but each CLI_ONLY command would still need its own help text. This per-command pattern matches how shared ops handle it via printOpHelp(). Init is the highest-stakes case because it's the only CLI_ONLY command that calls saveConfig(). Smoke test: from a directory with 1500 .md files, with GBRAIN_HOME pointed at a fresh tempdir: - Before fix: ~/.gbrain/config.json materialized with engine: 'pglite' - After fix: help text printed, no config dir created `bun run typecheck` clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/init.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/commands/init.ts b/src/commands/init.ts index 5fa08bbfa..6e31ba8b0 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -11,6 +11,20 @@ import { createEngine } from '../core/engine-factory.ts'; import { discoverOAuth, mintClientCredentialsToken, smokeTestMcp } from '../core/remote-mcp-probe.ts'; export async function runInit(args: string[]) { + // Help guard: cli.ts only routes --help to printOpHelp() for shared-op + // commands; CLI_ONLY commands (init, embed, etc.) fall through to their + // handler with --help in argv. Without this guard, `gbrain init --help` + // proceeds into the smart-detection branch below, scans cwd for .md files, + // and on a directory with 1000+ files (e.g. $HOME for someone whose brain + // and notes share a root) silently overwrites the existing Supabase config + // with a fresh PGLite brain at ~/.gbrain/brain.pglite. Confirmed in the + // wild — flipped a working `engine: postgres` config to `engine: pglite` + // on a brain with 10K+ pages. Help should never mutate state. + if (args.includes('--help') || args.includes('-h')) { + printInitHelp(); + return; + } + const isSupabase = args.includes('--supabase'); const isPGLite = args.includes('--pglite'); const isMcpOnly = args.includes('--mcp-only'); @@ -729,3 +743,48 @@ export function reportModStatus(): void { console.log('Soul audit: run `gbrain soul-audit` to customize agent identity'); console.log(''); } + +function printInitHelp() { + console.log(` +gbrain init — initialize a brain (PGLite or Supabase Postgres) + +USAGE + gbrain init [flags] + +ENGINE SELECTION (mutually exclusive) + --pglite Use embedded PGLite (zero-config, default for <1000 .md files) + --supabase Use Supabase Postgres (recommended for 1000+ files) + --url Use a manual Postgres connection string + --mcp-only Thin-client mode: connect to a remote gbrain MCP, no local engine + +OPTIONS + --force Overwrite an existing config (gated by default) + --non-interactive Don't prompt; use defaults + --migrate-only Apply pending schema migrations against the configured engine + without re-saving config (used by post-upgrade and orchestrators) + --json JSON output for status reporting + --path Override default brain path (PGLite only) + --key Provide an API key non-interactively (Supabase only) + --embedding-model + e.g. openai:text-embedding-3-large, voyage:voyage-multimodal-3 + --model Shorthand: pick recipe default for a provider + --embedding-dimensions + Embedding dimensions (must match the model) + --expansion-model + Model for query expansion (default: anthropic:claude-haiku) + --chat-model + Default subagent driver (v0.27+) + +EXAMPLES + gbrain init --pglite # Local-only, no API keys + gbrain init --supabase # Interactive Supabase setup + gbrain init --url postgresql://... # Use a custom Postgres + gbrain init --mcp-only --url https://... # Thin-client mode + +NOTES + - Bare \`gbrain init\` in a directory with 1000+ .md files defaults to Supabase + interactive setup. With <1000 files (or with --pglite explicitly), defaults + to PGLite at ~/.gbrain/brain.pglite. + - Existing config is preserved unless --force is passed. +`.trim()); +}