@productbrain/cli 0.1.0-beta.30 → 0.1.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +62 -178
  2. package/dist/__tests__/constants.test.js +1 -1
  3. package/dist/__tests__/constants.test.js.map +1 -1
  4. package/dist/__tests__/errors.test.d.ts +2 -0
  5. package/dist/__tests__/errors.test.d.ts.map +1 -0
  6. package/dist/__tests__/errors.test.js +117 -0
  7. package/dist/__tests__/errors.test.js.map +1 -0
  8. package/dist/__tests__/glossary.test.d.ts +2 -0
  9. package/dist/__tests__/glossary.test.d.ts.map +1 -0
  10. package/dist/__tests__/glossary.test.js +32 -0
  11. package/dist/__tests__/glossary.test.js.map +1 -0
  12. package/dist/__tests__/login.test.js +1 -1
  13. package/dist/__tests__/login.test.js.map +1 -1
  14. package/dist/__tests__/profiles.test.d.ts +2 -0
  15. package/dist/__tests__/profiles.test.d.ts.map +1 -0
  16. package/dist/__tests__/profiles.test.js +168 -0
  17. package/dist/__tests__/profiles.test.js.map +1 -0
  18. package/dist/commands/capture.d.ts.map +1 -1
  19. package/dist/commands/capture.js +3 -2
  20. package/dist/commands/capture.js.map +1 -1
  21. package/dist/commands/doctor.d.ts +9 -2
  22. package/dist/commands/doctor.d.ts.map +1 -1
  23. package/dist/commands/doctor.js +112 -25
  24. package/dist/commands/doctor.js.map +1 -1
  25. package/dist/commands/doctor.test.d.ts +2 -1
  26. package/dist/commands/doctor.test.d.ts.map +1 -1
  27. package/dist/commands/doctor.test.js +166 -3
  28. package/dist/commands/doctor.test.js.map +1 -1
  29. package/dist/commands/profile.d.ts +24 -0
  30. package/dist/commands/profile.d.ts.map +1 -0
  31. package/dist/commands/profile.js +82 -0
  32. package/dist/commands/profile.js.map +1 -0
  33. package/dist/commands/promote.d.ts.map +1 -1
  34. package/dist/commands/promote.js +3 -2
  35. package/dist/commands/promote.js.map +1 -1
  36. package/dist/formatters/promote.d.ts +1 -0
  37. package/dist/formatters/promote.d.ts.map +1 -1
  38. package/dist/formatters/promote.js +1 -0
  39. package/dist/formatters/promote.js.map +1 -1
  40. package/dist/index.js +240 -299
  41. package/dist/index.js.map +1 -1
  42. package/dist/lib/client.d.ts +2 -2
  43. package/dist/lib/client.d.ts.map +1 -1
  44. package/dist/lib/client.js +30 -11
  45. package/dist/lib/client.js.map +1 -1
  46. package/dist/lib/config.d.ts +9 -3
  47. package/dist/lib/config.d.ts.map +1 -1
  48. package/dist/lib/config.js +49 -12
  49. package/dist/lib/config.js.map +1 -1
  50. package/dist/lib/constants.d.ts +1 -1
  51. package/dist/lib/constants.d.ts.map +1 -1
  52. package/dist/lib/constants.js +1 -1
  53. package/dist/lib/constants.js.map +1 -1
  54. package/dist/lib/errors.d.ts +57 -0
  55. package/dist/lib/errors.d.ts.map +1 -0
  56. package/dist/lib/errors.js +65 -0
  57. package/dist/lib/errors.js.map +1 -0
  58. package/dist/lib/glossary.d.ts +19 -0
  59. package/dist/lib/glossary.d.ts.map +1 -0
  60. package/dist/lib/glossary.js +53 -0
  61. package/dist/lib/glossary.js.map +1 -0
  62. package/dist/lib/profiles.d.ts +34 -0
  63. package/dist/lib/profiles.d.ts.map +1 -0
  64. package/dist/lib/profiles.js +173 -0
  65. package/dist/lib/profiles.js.map +1 -0
  66. package/dist/lib/runner.d.ts +2 -0
  67. package/dist/lib/runner.d.ts.map +1 -1
  68. package/dist/lib/runner.js +33 -4
  69. package/dist/lib/runner.js.map +1 -1
  70. package/dist/lib/style.d.ts +65 -0
  71. package/dist/lib/style.d.ts.map +1 -0
  72. package/dist/lib/style.js +108 -0
  73. package/dist/lib/style.js.map +1 -0
  74. package/dist/lib/style.test.d.ts +7 -0
  75. package/dist/lib/style.test.d.ts.map +1 -0
  76. package/dist/lib/style.test.js +195 -0
  77. package/dist/lib/style.test.js.map +1 -0
  78. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,10 +7,11 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { dirname, join } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { Command } from 'commander';
10
+ import { Command, CommanderError } from 'commander';
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const cliPackageVersion = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
13
- import { setOutputMode } from './lib/runner.js';
13
+ import { setOutputMode, setQuietMode, isJsonMode } from './lib/runner.js';
14
+ import { CLIError, ErrorCode } from './lib/errors.js';
14
15
  import { runContext } from './commands/context.js';
15
16
  import { runGet, runGetMany } from './commands/get.js';
16
17
  import { runLogin } from './commands/login.js';
@@ -39,14 +40,67 @@ import { runVerify } from './commands/verify.js';
39
40
  import { runCodexPrep } from './commands/codex-prep.js';
40
41
  import { runCollectionsList, runCollectionsGet, runCollectionsAudit, runCollectionsExport } from './commands/collections.js';
41
42
  import { runDoctor } from './commands/doctor.js';
43
+ import { runProfileList, runProfileCreate, runProfileUse, runProfileDelete } from './commands/profile.js';
44
+ import { GLOSSARY, formatGlossary } from './lib/glossary.js';
45
+ import { hint, heading, bold } from './lib/style.js';
42
46
  const program = new Command();
47
+ /**
48
+ * Global error handler — single exit point for all CLI errors.
49
+ * CLIError: format with code + category + guidance.
50
+ * CommanderError: re-throw for --help / --version (exitCode 0), format others.
51
+ * Regular Error: wrap as INTERNAL.
52
+ */
53
+ function handleError(err) {
54
+ // Commander help/version exits with code 0 — let them through
55
+ if (err instanceof CommanderError && err.exitCode === 0) {
56
+ process.exit(0);
57
+ }
58
+ const json = isJsonMode();
59
+ if (err instanceof CLIError) {
60
+ if (json) {
61
+ process.stderr.write(JSON.stringify(err.toJSON()) + '\n');
62
+ }
63
+ else {
64
+ process.stderr.write(err.message + '\n');
65
+ if (err.guidance) {
66
+ process.stderr.write(err.guidance + '\n');
67
+ }
68
+ }
69
+ process.exit(1);
70
+ }
71
+ // CommanderError (bad usage, missing args, etc.)
72
+ if (err instanceof CommanderError) {
73
+ const cliErr = new CLIError(err.message, {
74
+ code: ErrorCode.VALIDATION_FAILED,
75
+ category: 'validation',
76
+ });
77
+ if (json) {
78
+ process.stderr.write(JSON.stringify(cliErr.toJSON()) + '\n');
79
+ }
80
+ else {
81
+ process.stderr.write(err.message + '\n');
82
+ }
83
+ process.exit(err.exitCode);
84
+ }
85
+ // Unknown / unstructured errors
86
+ const message = err instanceof Error ? err.message : String(err);
87
+ if (json) {
88
+ process.stderr.write(JSON.stringify({ error: message, code: 'INTERNAL', category: 'internal' }) + '\n');
89
+ }
90
+ else {
91
+ process.stderr.write(message + '\n');
92
+ }
93
+ process.exit(1);
94
+ }
43
95
  program
44
96
  .name('pb')
45
97
  .description('Product Brain — Chain knowledge + write-back CLI')
46
98
  .version(cliPackageVersion.version)
47
99
  // Global output mode flags (DEC-299, BET-181)
48
100
  .option('--json', 'Output machine-readable JSON (overrides TTY auto-detection)')
49
- .option('--pretty', 'Force human-readable output even when piped');
101
+ .option('--pretty', 'Force human-readable output even when piped')
102
+ .option('-q, --quiet', 'Suppress non-essential output (banners, hints, progress)')
103
+ .exitOverride(); // Throw CommanderError instead of calling process.exit directly
50
104
  // Apply global output mode BEFORE subcommand actions run
51
105
  program.hook('preAction', (thisCommand) => {
52
106
  const globalOpts = program.opts();
@@ -57,8 +111,43 @@ program.hook('preAction', (thisCommand) => {
57
111
  setOutputMode('pretty');
58
112
  }
59
113
  // else: 'auto' (default) — TTY detection handles it
114
+ if (globalOpts.quiet) {
115
+ setQuietMode(true);
116
+ }
60
117
  void thisCommand;
61
118
  });
119
+ // Default action — contextual guidance when `pb` is invoked with no command.
120
+ // `pb --help` still works because Commander intercepts --help before the action runs.
121
+ program.action(() => {
122
+ // If in JSON mode, output structured data
123
+ if (isJsonMode()) {
124
+ process.stdout.write(JSON.stringify({ hint: 'Run pb --help for available commands.' }) + '\n');
125
+ return;
126
+ }
127
+ const apiKey = process.env.PRODUCTBRAIN_API_KEY ?? '';
128
+ const hasKey = apiKey.startsWith('pb_sk_');
129
+ process.stdout.write('\n');
130
+ process.stdout.write(`${bold('Product Brain')}\n\n`);
131
+ if (!hasKey) {
132
+ process.stdout.write(`${heading('Get started')}\n\n`);
133
+ hint('pb login Save your API key');
134
+ hint('pb doctor Check configuration');
135
+ hint('pb setup Guided first-time setup');
136
+ process.stdout.write('\n');
137
+ hint('pb --help Show all commands');
138
+ }
139
+ else {
140
+ process.stdout.write(`${heading('Quick start')}\n\n`);
141
+ hint('pb orient -b Workspace overview');
142
+ hint('pb search <q> Search the Chain');
143
+ hint('pb capture <text> Capture knowledge');
144
+ hint('pb session start Start a write session');
145
+ process.stdout.write('\n');
146
+ hint('pb --help Show all commands');
147
+ hint('pb glossary Key terms');
148
+ }
149
+ process.stdout.write('\n');
150
+ });
62
151
  program
63
152
  .command('get <entry-id>')
64
153
  .description('Display full entry by ID (data, relations, last 10 history events)')
@@ -67,25 +156,13 @@ program
67
156
  program.commands.find((c) => c.name() === 'get')?.help();
68
157
  return;
69
158
  }
70
- try {
71
- await runGet({ entryId: entryId.trim() });
72
- }
73
- catch (err) {
74
- console.error(err instanceof Error ? err.message : String(err));
75
- process.exit(1);
76
- }
159
+ await runGet({ entryId: entryId.trim() });
77
160
  });
78
161
  program
79
162
  .command('get-many <entry-ids...>')
80
163
  .description('Fetch multiple entries in parallel (space-separated IDs)')
81
164
  .action(async (entryIds) => {
82
- try {
83
- await runGetMany({ entryIds: entryIds.map((id) => id.trim()).filter(Boolean) });
84
- }
85
- catch (err) {
86
- console.error(err instanceof Error ? err.message : String(err));
87
- process.exit(1);
88
- }
165
+ await runGetMany({ entryIds: entryIds.map((id) => id.trim()).filter(Boolean) });
89
166
  });
90
167
  program
91
168
  .command('context <entry-id>')
@@ -95,26 +172,14 @@ program
95
172
  program.commands.find((c) => c.name() === 'context')?.help();
96
173
  return;
97
174
  }
98
- try {
99
- await runContext({ entryId: entryId.trim() });
100
- }
101
- catch (err) {
102
- console.error(err instanceof Error ? err.message : String(err));
103
- process.exit(1);
104
- }
175
+ await runContext({ entryId: entryId.trim() });
105
176
  });
106
177
  program
107
178
  .command('changes')
108
179
  .description('Detect entries modified and relations created since a given time (BET-239)')
109
180
  .requiredOption('--since <duration>', 'Time window: 1h, 6h, 1d, 7d, 30d')
110
181
  .action(async (opts) => {
111
- try {
112
- await runChanges({ since: opts.since });
113
- }
114
- catch (err) {
115
- console.error(err instanceof Error ? err.message : String(err));
116
- process.exit(1);
117
- }
182
+ await runChanges({ since: opts.since });
118
183
  });
119
184
  program
120
185
  .command('walk <entry-id>')
@@ -123,36 +188,26 @@ program
123
188
  .option('--direction <dir>', 'Traversal direction: outgoing or incoming (default outgoing)', 'outgoing')
124
189
  .option('-t, --type <relation-type>', 'Filter by relation type (e.g. depends_on, informs, governs)')
125
190
  .action(async (entryId, opts) => {
126
- try {
127
- const depth = parseInt(opts.depth, 10);
128
- if (isNaN(depth) || depth < 1 || depth > 4) {
129
- console.error('--depth must be between 1 and 4.');
130
- process.exit(1);
131
- }
132
- await runChainWalk({
133
- entryId: entryId.trim(),
134
- depth,
135
- direction: opts.direction,
136
- type: opts.type,
191
+ const depth = parseInt(opts.depth, 10);
192
+ if (isNaN(depth) || depth < 1 || depth > 4) {
193
+ throw new CLIError('--depth must be between 1 and 4.', {
194
+ code: ErrorCode.VALIDATION_FAILED,
195
+ category: 'validation',
137
196
  });
138
197
  }
139
- catch (err) {
140
- console.error(err instanceof Error ? err.message : String(err));
141
- process.exit(1);
142
- }
198
+ await runChainWalk({
199
+ entryId: entryId.trim(),
200
+ depth,
201
+ direction: opts.direction,
202
+ type: opts.type,
203
+ });
143
204
  });
144
205
  program
145
206
  .command('cross-cut')
146
207
  .description('Structural aggregation — all relations of a given type grouped by source collection (BET-239)')
147
208
  .requiredOption('--type <relation-type>', 'Relation type to scan (e.g. part_of, informs, governs)')
148
209
  .action(async (opts) => {
149
- try {
150
- await runCrossCut({ type: opts.type });
151
- }
152
- catch (err) {
153
- console.error(err instanceof Error ? err.message : String(err));
154
- process.exit(1);
155
- }
210
+ await runCrossCut({ type: opts.type });
156
211
  });
157
212
  program
158
213
  .command('brief [type]')
@@ -165,27 +220,20 @@ program
165
220
  .option('--since-last', 'Compare against last brief run (incremental mode)')
166
221
  .option('--since <timestamp>', 'ISO 8601 timestamp for delta type (e.g. 2026-03-24T00:00:00Z)')
167
222
  .action(async (type, opts) => {
168
- try {
169
- // If positional arg is a compound type, route to compound brief
170
- if (type && isCompoundType(type)) {
171
- await runCompoundBrief({ type, since: opts.since });
172
- return;
173
- }
174
- // Otherwise, require --skill for incremental brief
175
- if (!opts.skill) {
176
- console.error('Usage:\n' +
177
- ' pb brief --skill <name> Incremental delta\n' +
178
- ' pb brief steering Compound steering brief\n' +
179
- ' pb brief confidence Compound confidence pass\n' +
180
- ' pb brief delta Compound delta sync');
181
- process.exit(1);
182
- }
183
- await runBrief({ skill: opts.skill, sinceLast: opts.sinceLast });
223
+ // If positional arg is a compound type, route to compound brief
224
+ if (type && isCompoundType(type)) {
225
+ await runCompoundBrief({ type, since: opts.since });
226
+ return;
184
227
  }
185
- catch (err) {
186
- console.error(err instanceof Error ? err.message : String(err));
187
- process.exit(1);
228
+ // Otherwise, require --skill for incremental brief
229
+ if (!opts.skill) {
230
+ throw new CLIError('Usage:\n' +
231
+ ' pb brief --skill <name> Incremental delta\n' +
232
+ ' pb brief steering Compound steering brief\n' +
233
+ ' pb brief confidence Compound confidence pass\n' +
234
+ ' pb brief delta Compound delta sync', { code: ErrorCode.VALIDATION_FAILED, category: 'validation' });
188
235
  }
236
+ await runBrief({ skill: opts.skill, sinceLast: opts.sinceLast });
189
237
  });
190
238
  program
191
239
  .command('search [query...]')
@@ -196,13 +244,7 @@ program
196
244
  program.commands.find((c) => c.name() === 'search')?.help();
197
245
  return;
198
246
  }
199
- try {
200
- await runSearch({ query });
201
- }
202
- catch (err) {
203
- console.error(err instanceof Error ? err.message : String(err));
204
- process.exit(1);
205
- }
247
+ await runSearch({ query });
206
248
  });
207
249
  program
208
250
  .command('orient')
@@ -212,13 +254,7 @@ program
212
254
  // SYNC: domain list must match TaskDomain in convex/mcpKnowledge/startupResolver.ts
213
255
  .option('-s, --scope <domain>', 'Domain scope for governance filtering. Valid values: auth, governance, architecture, product, data-foundation, chainwork, capture-pipeline, ingestion, intelligence-and-operations, review-and-learning, general')
214
256
  .action(async (opts) => {
215
- try {
216
- await runOrient({ brief: opts.brief, task: opts.task?.trim() || undefined, scope: opts.scope?.trim() || undefined });
217
- }
218
- catch (err) {
219
- console.error(err instanceof Error ? err.message : String(err));
220
- process.exit(1);
221
- }
257
+ await runOrient({ brief: opts.brief, task: opts.task?.trim() || undefined, scope: opts.scope?.trim() || undefined });
222
258
  });
223
259
  program
224
260
  .command('handshake')
@@ -229,17 +265,11 @@ program
229
265
  .option('--level <level>', 'With --init: trust level (guide|work|silent|full-trust). Without --init: content tier (beginner|intermediate|expert)')
230
266
  .option('--generate', 'Fetch governance entries from the Chain and merge generated rules (BET-286)')
231
267
  .action(async (opts) => {
232
- try {
233
- if (opts.init) {
234
- await runHandshakeInit({ level: opts.level, dryRun: opts.dryRun });
235
- return;
236
- }
237
- await runHandshake({ force: opts.force, dryRun: opts.dryRun, level: opts.level, generate: opts.generate });
238
- }
239
- catch (err) {
240
- console.error(err instanceof Error ? err.message : String(err));
241
- process.exit(1);
268
+ if (opts.init) {
269
+ await runHandshakeInit({ level: opts.level, dryRun: opts.dryRun });
270
+ return;
242
271
  }
272
+ await runHandshake({ force: opts.force, dryRun: opts.dryRun, level: opts.level, generate: opts.generate });
243
273
  });
244
274
  program
245
275
  .command('codex-prep <task...>')
@@ -251,49 +281,27 @@ program
251
281
  program.commands.find((c) => c.name() === 'codex-prep')?.help();
252
282
  return;
253
283
  }
254
- try {
255
- await runCodexPrep({ task, dryRun: opts.dryRun });
256
- }
257
- catch (err) {
258
- console.error(err instanceof Error ? err.message : String(err));
259
- process.exit(1);
260
- }
284
+ await runCodexPrep({ task, dryRun: opts.dryRun });
261
285
  });
262
286
  program
263
287
  .command('login')
264
288
  .description('Save your API key to ~/.config/productbrain/.env (works from any directory)')
265
289
  .action(async () => {
266
- try {
267
- await runLogin();
268
- }
269
- catch (err) {
270
- console.error(err instanceof Error ? err.message : String(err));
271
- process.exit(1);
272
- }
290
+ await runLogin();
273
291
  });
274
292
  program
275
293
  .command('doctor')
276
294
  .description('Check CLI configuration and connectivity')
277
- .action(async () => {
278
- try {
279
- await runDoctor();
280
- }
281
- catch (err) {
282
- console.error(err instanceof Error ? err.message : String(err));
283
- process.exit(1);
284
- }
295
+ .option('--fix', 'Auto-repair common configuration issues')
296
+ .option('--dry-run', 'Preview what --fix would do without changing anything')
297
+ .action(async (opts) => {
298
+ await runDoctor({ fix: opts.fix, dryRun: opts.dryRun });
285
299
  });
286
300
  program
287
301
  .command('setup')
288
302
  .description('Guided first-time setup: account, login, workspace, first capture')
289
303
  .action(async () => {
290
- try {
291
- await runSetup();
292
- }
293
- catch (err) {
294
- console.error(err instanceof Error ? err.message : String(err));
295
- process.exit(1);
296
- }
304
+ await runSetup();
297
305
  });
298
306
  // --- Write commands (require active session) ---
299
307
  const sessionCmd = program
@@ -304,38 +312,20 @@ sessionCmd
304
312
  .description('Start a tracked write session (opens session, refreshes context)')
305
313
  .option('--json', 'Output machine-readable JSON (deprecated: use global --json flag)')
306
314
  .action(async (opts) => {
307
- try {
308
- await runSessionStart({ json: opts.json });
309
- }
310
- catch (err) {
311
- console.error(err instanceof Error ? err.message : String(err));
312
- process.exit(1);
313
- }
315
+ await runSessionStart({ json: opts.json });
314
316
  });
315
317
  sessionCmd
316
318
  .command('id')
317
319
  .description('Print current session ID to stdout (machine-readable, TEN-707)')
318
320
  .action(() => {
319
- try {
320
- runSessionId();
321
- }
322
- catch (err) {
323
- console.error(err instanceof Error ? err.message : String(err));
324
- process.exit(1);
325
- }
321
+ runSessionId();
326
322
  });
327
323
  sessionCmd
328
324
  .command('close')
329
325
  .description('Close the active session (wrapup, refresh context)')
330
326
  .option('--force', 'Clear local session state even if server close fails')
331
327
  .action(async (opts) => {
332
- try {
333
- await runSessionClose({ force: opts.force });
334
- }
335
- catch (err) {
336
- console.error(err instanceof Error ? err.message : String(err));
337
- process.exit(1);
338
- }
328
+ await runSessionClose({ force: opts.force });
339
329
  });
340
330
  program
341
331
  .command('capture [text...]')
@@ -354,23 +344,17 @@ program
354
344
  program.commands.find((c) => c.name() === 'capture')?.help();
355
345
  return;
356
346
  }
357
- try {
358
- await runCapture({
359
- text: text || opts.name || '',
360
- name: opts.name,
361
- description: opts.description,
362
- collection: opts.collection,
363
- link: opts.link,
364
- type: opts.type,
365
- sourceRef: opts.sourceRef,
366
- sourceExcerpt: opts.sourceExcerpt,
367
- json: opts.json,
368
- });
369
- }
370
- catch (err) {
371
- console.error(err instanceof Error ? err.message : String(err));
372
- process.exit(1);
373
- }
347
+ await runCapture({
348
+ text: text || opts.name || '',
349
+ name: opts.name,
350
+ description: opts.description,
351
+ collection: opts.collection,
352
+ link: opts.link,
353
+ type: opts.type,
354
+ sourceRef: opts.sourceRef,
355
+ sourceExcerpt: opts.sourceExcerpt,
356
+ json: opts.json,
357
+ });
374
358
  });
375
359
  // --- Update command (TEN-703) ---
376
360
  program
@@ -382,20 +366,14 @@ program
382
366
  .option('--workflow-status <status>', 'Set workflow status (server-validated)')
383
367
  .option('--note <text>', 'Change note for history')
384
368
  .action(async (entryId, opts) => {
385
- try {
386
- await runUpdate({
387
- entryId,
388
- field: opts.field,
389
- name: opts.name,
390
- status: opts.status,
391
- workflowStatus: opts.workflowStatus,
392
- note: opts.note,
393
- });
394
- }
395
- catch (err) {
396
- console.error(err instanceof Error ? err.message : String(err));
397
- process.exit(1);
398
- }
369
+ await runUpdate({
370
+ entryId,
371
+ field: opts.field,
372
+ name: opts.name,
373
+ status: opts.status,
374
+ workflowStatus: opts.workflowStatus,
375
+ note: opts.note,
376
+ });
399
377
  });
400
378
  // --- Promote command (Wave 2 CLI Polish) ---
401
379
  program
@@ -406,26 +384,14 @@ program
406
384
  program.commands.find((c) => c.name() === 'verify')?.help();
407
385
  return;
408
386
  }
409
- try {
410
- await runVerify({ entryId: entryId.trim() });
411
- }
412
- catch (err) {
413
- console.error(err instanceof Error ? err.message : String(err));
414
- process.exit(1);
415
- }
387
+ await runVerify({ entryId: entryId.trim() });
416
388
  });
417
389
  program
418
390
  .command('promote <entry-id>')
419
391
  .description('Promote entry from draft to active (commit to SSOT)')
420
392
  .option('-m, --message <text>', 'Commit message')
421
393
  .action(async (entryId, opts) => {
422
- try {
423
- await runPromote({ entryId, message: opts.message });
424
- }
425
- catch (err) {
426
- console.error(err instanceof Error ? err.message : String(err));
427
- process.exit(1);
428
- }
394
+ await runPromote({ entryId, message: opts.message });
429
395
  });
430
396
  // --- Relate / Unrelate commands (TEN-704) ---
431
397
  program
@@ -433,25 +399,13 @@ program
433
399
  .description('Add a typed relation between two entries (requires active session)')
434
400
  .option('--if-missing', 'Only create relation if it does not already exist')
435
401
  .action(async (fromId, type, toId, opts) => {
436
- try {
437
- await runRelate({ fromId, type, toId, ifMissing: opts.ifMissing });
438
- }
439
- catch (err) {
440
- console.error(err instanceof Error ? err.message : String(err));
441
- process.exit(1);
442
- }
402
+ await runRelate({ fromId, type, toId, ifMissing: opts.ifMissing });
443
403
  });
444
404
  program
445
405
  .command('unrelate <from-id> <type> <to-id>')
446
406
  .description('Remove a typed relation between two entries (requires active session)')
447
407
  .action(async (fromId, type, toId) => {
448
- try {
449
- await runUnrelate({ fromId, type, toId });
450
- }
451
- catch (err) {
452
- console.error(err instanceof Error ? err.message : String(err));
453
- process.exit(1);
454
- }
408
+ await runUnrelate({ fromId, type, toId });
455
409
  });
456
410
  // --- Ingest command (BET-81) ---
457
411
  program
@@ -461,23 +415,19 @@ program
461
415
  .option('--resume', 'Skip files whose sourceRef is already committed in staging.')
462
416
  .option('--concurrency <n>', 'Process up to N files in parallel (default 1, max 5).', '1')
463
417
  .action(async (pattern, opts) => {
464
- try {
465
- const concurrency = parseInt(opts.concurrency ?? '1', 10);
466
- if (Number.isNaN(concurrency) || concurrency < 1) {
467
- console.error('--concurrency must be a positive integer.');
468
- process.exit(1);
469
- }
470
- await runIngest({
471
- pattern,
472
- dryRun: opts.dryRun,
473
- resume: opts.resume,
474
- concurrency,
418
+ const concurrency = parseInt(opts.concurrency ?? '1', 10);
419
+ if (Number.isNaN(concurrency) || concurrency < 1) {
420
+ throw new CLIError('--concurrency must be a positive integer.', {
421
+ code: ErrorCode.VALIDATION_FAILED,
422
+ category: 'validation',
475
423
  });
476
424
  }
477
- catch (err) {
478
- console.error(err instanceof Error ? err.message : String(err));
479
- process.exit(1);
480
- }
425
+ await runIngest({
426
+ pattern,
427
+ dryRun: opts.dryRun,
428
+ resume: opts.resume,
429
+ concurrency,
430
+ });
481
431
  });
482
432
  // --- Fields command (BET-181 Slice 2) ---
483
433
  program
@@ -488,13 +438,7 @@ program
488
438
  program.commands.find((c) => c.name() === 'fields')?.help();
489
439
  return;
490
440
  }
491
- try {
492
- await runFields({ collectionSlug: collection.trim() });
493
- }
494
- catch (err) {
495
- console.error(err instanceof Error ? err.message : String(err));
496
- process.exit(1);
497
- }
441
+ await runFields({ collectionSlug: collection.trim() });
498
442
  });
499
443
  // --- Constellation command (BET-181 Slice 3) ---
500
444
  program
@@ -505,13 +449,7 @@ program
505
449
  program.commands.find((c) => c.name() === 'constellation')?.help();
506
450
  return;
507
451
  }
508
- try {
509
- await runConstellation({ entryId: entryId.trim() });
510
- }
511
- catch (err) {
512
- console.error(err instanceof Error ? err.message : String(err));
513
- process.exit(1);
514
- }
452
+ await runConstellation({ entryId: entryId.trim() });
515
453
  });
516
454
  // --- Audit command (BET-182 Slice 2) ---
517
455
  program
@@ -522,19 +460,13 @@ program
522
460
  .option('--fix', 'Auto-execute exact fixes via pb update, rerun once')
523
461
  .option('--verbose', 'Show all gates including PASS detail')
524
462
  .action(async (entryIds, opts) => {
525
- try {
526
- await runAudit({
527
- entryIds: entryIds.map((id) => id.trim()).filter(Boolean),
528
- phase: opts.phase,
529
- gate: opts.gate,
530
- fix: opts.fix,
531
- verbose: opts.verbose,
532
- });
533
- }
534
- catch (err) {
535
- console.error(err instanceof Error ? err.message : String(err));
536
- process.exit(1);
537
- }
463
+ await runAudit({
464
+ entryIds: entryIds.map((id) => id.trim()).filter(Boolean),
465
+ phase: opts.phase,
466
+ gate: opts.gate,
467
+ fix: opts.fix,
468
+ verbose: opts.verbose,
469
+ });
538
470
  });
539
471
  program
540
472
  .command('brand-pack')
@@ -549,13 +481,7 @@ program
549
481
  .command('proposals')
550
482
  .description('List open consent proposals with expiry countdown (BET-221)')
551
483
  .action(async () => {
552
- try {
553
- await runProposals();
554
- }
555
- catch (err) {
556
- console.error(err instanceof Error ? err.message : String(err));
557
- process.exit(1);
558
- }
484
+ await runProposals();
559
485
  });
560
486
  program
561
487
  .command('accept [proposal-id]')
@@ -563,29 +489,19 @@ program
563
489
  .option('-a, --auto', 'Auto-approve all open proposals (skip contradictions)')
564
490
  .action(async (proposalId, opts) => {
565
491
  if (!proposalId && !opts.auto) {
566
- process.stderr.write('Usage: pb accept <proposal-id> or pb accept --auto\n');
567
- process.exit(1);
568
- }
569
- try {
570
- await runAccept({ proposalId, auto: opts.auto });
571
- }
572
- catch (err) {
573
- console.error(err instanceof Error ? err.message : String(err));
574
- process.exit(1);
492
+ throw new CLIError('Usage: pb accept <proposal-id> or pb accept --auto', {
493
+ code: ErrorCode.VALIDATION_FAILED,
494
+ category: 'validation',
495
+ });
575
496
  }
497
+ await runAccept({ proposalId, auto: opts.auto });
576
498
  });
577
499
  program
578
500
  .command('reject <proposal-id>')
579
501
  .description('Reject a consent proposal with a required reason (BR-7)')
580
502
  .requiredOption('-r, --reason <text>', 'Reason for rejection (required)')
581
503
  .action(async (proposalId, opts) => {
582
- try {
583
- await runReject({ proposalId, reason: opts.reason });
584
- }
585
- catch (err) {
586
- console.error(err instanceof Error ? err.message : String(err));
587
- process.exit(1);
588
- }
504
+ await runReject({ proposalId, reason: opts.reason });
589
505
  });
590
506
  // --- Collections command (BET-280 Slice 1) ---
591
507
  const collectionsCmd = program
@@ -595,13 +511,7 @@ collectionsCmd
595
511
  .command('list')
596
512
  .description('List all collections — slug, name, field count, icon')
597
513
  .action(async () => {
598
- try {
599
- await runCollectionsList();
600
- }
601
- catch (err) {
602
- console.error(err instanceof Error ? err.message : String(err));
603
- process.exit(1);
604
- }
514
+ await runCollectionsList();
605
515
  });
606
516
  collectionsCmd
607
517
  .command('get <slug>')
@@ -611,37 +521,68 @@ collectionsCmd
611
521
  program.commands.find((c) => c.name() === 'collections')?.help();
612
522
  return;
613
523
  }
614
- try {
615
- await runCollectionsGet({ slug: slug.trim() });
616
- }
617
- catch (err) {
618
- console.error(err instanceof Error ? err.message : String(err));
619
- process.exit(1);
620
- }
524
+ await runCollectionsGet({ slug: slug.trim() });
621
525
  });
622
526
  collectionsCmd
623
527
  .command('audit')
624
528
  .description('Collection health report — classification, icon, displayHint coverage, schema gaps')
625
529
  .action(async () => {
626
- try {
627
- await runCollectionsAudit();
628
- }
629
- catch (err) {
630
- console.error(err instanceof Error ? err.message : String(err));
631
- process.exit(1);
632
- }
530
+ await runCollectionsAudit();
633
531
  });
634
532
  collectionsCmd
635
533
  .command('export')
636
534
  .description('Export all system_collection_definitions with full classification metadata (admin only)')
637
535
  .action(async () => {
638
- try {
639
- await runCollectionsExport();
640
- }
641
- catch (err) {
642
- console.error(err instanceof Error ? err.message : String(err));
643
- process.exit(1);
644
- }
536
+ await runCollectionsExport();
645
537
  });
646
- program.parse();
538
+ // --- Profile commands (WP-302 Slice 2) ---
539
+ const profileCmd = program
540
+ .command('profile')
541
+ .description('Manage workspace profiles (WP-302: multi-workspace support)');
542
+ profileCmd
543
+ .command('list')
544
+ .description('List all profiles, mark active one')
545
+ .action(async () => {
546
+ await runProfileList();
547
+ });
548
+ profileCmd
549
+ .command('create <name>')
550
+ .description('Create a new profile with an API key')
551
+ .requiredOption('--api-key <key>', 'API key (pb_sk_...)')
552
+ .option('--url <url>', 'Convex site URL (defaults to production)')
553
+ .action(async (name, opts) => {
554
+ await runProfileCreate({ name, apiKey: opts.apiKey, url: opts.url });
555
+ });
556
+ profileCmd
557
+ .command('use <name>')
558
+ .description('Switch active profile (closes any active session)')
559
+ .action(async (name) => {
560
+ await runProfileUse({ name });
561
+ });
562
+ profileCmd
563
+ .command('delete <name>')
564
+ .description('Delete a profile (cannot delete active or last profile)')
565
+ .action(async (name) => {
566
+ await runProfileDelete({ name });
567
+ });
568
+ // --- Glossary command (WP-302 Slice 4) ---
569
+ program
570
+ .command('glossary')
571
+ .description('Show key Product Brain CLI terms and definitions')
572
+ .action(() => {
573
+ if (isJsonMode()) {
574
+ process.stdout.write(JSON.stringify(GLOSSARY) + '\n');
575
+ return;
576
+ }
577
+ const lines = [
578
+ '',
579
+ heading('Glossary'),
580
+ '',
581
+ formatGlossary(),
582
+ '',
583
+ ];
584
+ process.stdout.write(lines.join('\n') + '\n');
585
+ });
586
+ // Parse with global error handler — all uncaught errors route through handleError
587
+ program.parseAsync().catch(handleError);
647
588
  //# sourceMappingURL=index.js.map