diff --git a/src/commands/init.ts b/src/commands/init.ts index 14e33f6cf..3a2ff7e08 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -26,6 +26,8 @@ export async function runInit(args: string[]) { return; } + validateInitFlags(args); + const isSupabase = args.includes('--supabase'); const isPGLite = args.includes('--pglite'); const isMcpOnly = args.includes('--mcp-only'); @@ -151,6 +153,65 @@ export async function runInit(args: string[]) { return initPostgres({ databaseUrl, jsonOutput, apiKey, aiOpts, schemaPack, skipEmbedCheck }); } +const INIT_BOOLEAN_FLAGS = new Set([ + '--pglite', + '--supabase', + '--mcp-only', + '--force', + '--non-interactive', + '--migrate-only', + '--json', + '--no-embedding', + '--skip-embed-check', +]); + +const INIT_VALUE_FLAGS = new Set([ + '--url', + '--key', + '--path', + '--schema-pack', + '--embedding-model', + '--model', + '--embedding-dimensions', + '--expansion-model', + '--chat-model', + '--mcp-url', + '--issuer-url', + '--oauth-client-id', + '--oauth-client-secret', +]); + +function validateInitFlags(args: string[]) { + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (!arg.startsWith('-')) continue; + + if (INIT_BOOLEAN_FLAGS.has(arg)) continue; + + if (INIT_VALUE_FLAGS.has(arg)) { + if (i + 1 >= args.length || args[i + 1].startsWith('-')) { + failInitFlag(`gbrain init: ${arg} requires a value`, args.includes('--json')); + } + i += 1; + continue; + } + + if (arg.startsWith('--')) { + failInitFlag(`gbrain init: unknown flag ${arg}`, args.includes('--json')); + } + } +} + +function failInitFlag(message: string, jsonOutput: boolean): never { + if (jsonOutput) { + console.log(JSON.stringify({ status: 'error', reason: 'invalid_flag', message })); + } else { + console.error(message); + console.error('Run `gbrain init --help` for supported flags.'); + } + process.exit(1); +} + interface ResolveAIOptionsArgs { verbose: string | null; // --embedding-model shorthand: string | null; // --model diff --git a/test/init-migrate-only.test.ts b/test/init-migrate-only.test.ts index 2f06001ec..a3732e5f1 100644 --- a/test/init-migrate-only.test.ts +++ b/test/init-migrate-only.test.ts @@ -57,6 +57,25 @@ afterEach(() => { }); describe('gbrain init --migrate-only — error paths', () => { + test('rejects unknown flags before any migrate-only side effects', () => { + const result = run(['init', '--migrate-only', '--dry-run']); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('unknown flag --dry-run'); + // Unknown safety flags must not fall through to the migration path. + expect(result.stderr).not.toContain('No brain configured'); + expect(existsSync(join(tmp, '.gbrain', 'config.json'))).toBe(false); + }); + + test('unknown flags respect --json output', () => { + const result = run(['init', '--migrate-only', '--dry-run', '--json']); + expect(result.exitCode).toBe(1); + const lines = result.stdout.split('\n').filter((l: string) => l.trim().startsWith('{')); + const parsed = JSON.parse(lines[lines.length - 1]); + expect(parsed.status).toBe('error'); + expect(parsed.reason).toBe('invalid_flag'); + expect(parsed.message).toContain('unknown flag --dry-run'); + }); + test('errors with clear message when no config exists', () => { const result = run(['init', '--migrate-only']); expect(result.exitCode).toBe(1);