@polderlabs/bizar 10.28.1 → 10.29.1

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.
package/cli/bin.mjs CHANGED
@@ -113,6 +113,9 @@ function showHelp() {
113
113
  restore Restore BizarHarness from a backup
114
114
  validate Validate the Bizar install
115
115
  setup-provider Configure the global provider used by Bizar and Claude Code
116
+ advisor Configure Claude Code's advisor tool
117
+ orchestrator Manage multi-select modelPicker entries for Claude Code
118
+ statusline Customized Claude Code status bar (render/install/remove/show/preview)
116
119
  release-provenance Generate SBOM + provenance + minisig for a release (audit #83)
117
120
  verify-release Verify a release artifact set against the pinned allowlist
118
121
  spec-list List SDK schemas, policy docs, and mirror sync status (audit #84)
@@ -297,6 +300,57 @@ async function main() {
297
300
  break;
298
301
  }
299
302
 
303
+ case 'advisor': {
304
+ const mod = await importCommand('advisor');
305
+ if (!mod) {
306
+ console.error(chalk.red(` ✗ Could not load advisor command module`));
307
+ process.exit(EXIT_ERROR);
308
+ return;
309
+ }
310
+ dbg('loaded command module:', 'advisor');
311
+ const found = await mod.run(cmd, cmdArgs, isHelpRequest);
312
+ if (!found) {
313
+ console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
314
+ showHelp();
315
+ process.exit(EXIT_ERROR);
316
+ }
317
+ break;
318
+ }
319
+
320
+ case 'orchestrator': {
321
+ const mod = await importCommand('orchestrator');
322
+ if (!mod) {
323
+ console.error(chalk.red(` ✗ Could not load orchestrator command module`));
324
+ process.exit(EXIT_ERROR);
325
+ return;
326
+ }
327
+ dbg('loaded command module:', 'orchestrator');
328
+ const found = await mod.run(cmd, cmdArgs, isHelpRequest);
329
+ if (!found) {
330
+ console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
331
+ showHelp();
332
+ process.exit(EXIT_ERROR);
333
+ }
334
+ break;
335
+ }
336
+
337
+ case 'statusline': {
338
+ const mod = await importCommand('statusline');
339
+ if (!mod) {
340
+ console.error(chalk.red(` ✗ Could not load statusline command module`));
341
+ process.exit(EXIT_ERROR);
342
+ return;
343
+ }
344
+ dbg('loaded command module:', 'statusline');
345
+ const found = await mod.run(cmd, cmdArgs, isHelpRequest);
346
+ if (!found) {
347
+ console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
348
+ showHelp();
349
+ process.exit(EXIT_ERROR);
350
+ }
351
+ break;
352
+ }
353
+
300
354
  case 'team':
301
355
  case 'subagent':
302
356
  case 'run': {
@@ -0,0 +1,274 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ export const ADVISOR_PAIRINGS = {
7
+ 'haiku-4-5': ['fable', 'opus-4-5', 'sonnet-4-5'],
8
+ 'sonnet-4-6': ['fable', 'opus-4-5', 'sonnet-4-5'],
9
+ 'sonnet-5': ['fable', 'opus-4-5', 'sonnet-5'],
10
+ 'opus-4-6': ['fable', 'opus-4-5', 'sonnet-5'],
11
+ 'opus-4-7+': ['fable', 'opus-4-7+'],
12
+ 'fable-5': ['fable-5'],
13
+ 'fable-5-1': ['fable-5-1'],
14
+ };
15
+
16
+ function modelMatches(modelId, pattern) {
17
+ const id = String(modelId ?? '').toLowerCase();
18
+ const hasPlus = pattern.endsWith('+');
19
+ const prefix = pattern.toLowerCase().replace(/\+$/, '');
20
+ return id === prefix || id.startsWith(`${prefix}-`) || id.includes(`-${prefix}-`) || (hasPlus && (id.includes(prefix) || id.includes(`-${prefix}`))) || (hasPlus && prefix === 'opus-4-7' && /opus-4-[7-9]/.test(id));
21
+ }
22
+
23
+ function mainPairingKey(modelId) {
24
+ const id = String(modelId ?? '').toLowerCase();
25
+ if (/opus-(?:[5-9]|4-(?:[7-9]|[1-9][0-9]+))/.test(id)) return 'opus-4-7+';
26
+ return Object.keys(ADVISOR_PAIRINGS)
27
+ .sort((a, b) => b.length - a.length)
28
+ .find((key) => modelMatches(id, key));
29
+ }
30
+
31
+ export function validateAdvisorPairing(mainId, advisorId) {
32
+ const mainKey = mainPairingKey(mainId);
33
+ if (!mainKey) return { verdict: 'unknown-main', accepted: [] };
34
+ const accepted = ADVISOR_PAIRINGS[mainKey];
35
+ const verdict = accepted.some((pattern) => modelMatches(advisorId, pattern)) ? 'ok' : 'rejected';
36
+ return { verdict, accepted };
37
+ }
38
+
39
+ export function settingsPath() {
40
+ const root = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude');
41
+ return join(root, 'settings.json');
42
+ }
43
+
44
+ export function readSettings(path = settingsPath()) {
45
+ if (!existsSync(path)) return {};
46
+ return JSON.parse(readFileSync(path, 'utf8'));
47
+ }
48
+
49
+ export function resolveMainModel(settings) {
50
+ const env = settings?.env ?? {};
51
+ return env.ANTHROPIC_MODEL || env.ANTHROPIC_DEFAULT_OPUS_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || env.ANTHROPIC_DEFAULT_HAIKU_MODEL || env.ANTHROPIC_DEFAULT_MODEL;
52
+ }
53
+
54
+ export function updateAdvisorSettings(current, options) {
55
+ const next = { ...current, env: { ...(current?.env ?? {}) } };
56
+ if (options.action === 'pick') next.advisorModel = options.model;
57
+ if (options.action === 'disable') next.env.CLAUDE_CODE_DISABLE_ADVISOR_TOOL = '1';
58
+ if (options.action === 'clear') {
59
+ delete next.advisorModel;
60
+ delete next.env.CLAUDE_CODE_DISABLE_ADVISOR_TOOL;
61
+ }
62
+ return next;
63
+ }
64
+
65
+ export function parseAdvisorArgs(args = []) {
66
+ const nonFlags = args.filter((arg) => !arg.startsWith('-'));
67
+ return {
68
+ subcommand: nonFlags[0] || 'pick',
69
+ help: args.includes('--help') || args.includes('-h'),
70
+ list: args.includes('--list'),
71
+ json: args.includes('--json'),
72
+ yes: args.includes('--yes'),
73
+ };
74
+ }
75
+
76
+ export async function fetchAdvisorEndpointModels(url, authToken, apiKey) {
77
+ const endpoint = `${url.replace(/\/$/, '')}/models`;
78
+ const headers = { Accept: 'application/json' };
79
+ if (apiKey) headers['x-api-key'] = apiKey;
80
+ else if (authToken) headers.Authorization = `Bearer ${authToken}`;
81
+ const response = await fetch(endpoint, { headers });
82
+ if (!response.ok) throw new Error(`Models endpoint returned ${response.status} ${response.statusText}`);
83
+ const body = await response.json();
84
+ return Array.isArray(body) ? body : (body.data ?? []);
85
+ }
86
+
87
+ function providerName(url) {
88
+ const lower = url.toLowerCase();
89
+ if (lower.includes('bedrock') || lower.includes('amazonaws.com')) return 'AWS Bedrock';
90
+ if (lower.includes('vertex') || lower.includes('googleapis.com')) return 'GCP Vertex';
91
+ if (lower.includes('foundry') || lower.includes('azure.com')) return 'Microsoft Foundry';
92
+ return url;
93
+ }
94
+
95
+ function isUnsupportedProvider(url) {
96
+ const lower = url.toLowerCase();
97
+ return lower.includes('bedrock') || lower.includes('vertex') || lower.includes('foundry') || lower.includes('amazonaws.com') || lower.includes('googleapis.com') || lower.includes('azure.com');
98
+ }
99
+
100
+ export function isAgentContext() {
101
+ return process.env.BIZAR_AGENT !== undefined || process.env.CLAUDE_CODE_AGENT_NAME !== undefined;
102
+ }
103
+
104
+ function fableWarning(model) {
105
+ return model?.toLowerCase().includes('fable')
106
+ ? '⚠ Fable advisors require one-time consent to bill usage credits. Run: claude (then type /advisor off and /advisor <fable> to consent once).'
107
+ : null;
108
+ }
109
+
110
+ function writeSettings(path, settings) {
111
+ mkdirSync(dirname(path), { recursive: true });
112
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
113
+ }
114
+
115
+ function eligibleModels(models, mainModel) {
116
+ const key = mainPairingKey(mainModel);
117
+ if (!key) return { models, notice: '⚠ could not determine main model — showing all options' };
118
+ const accepted = ADVISOR_PAIRINGS[key];
119
+ const filtered = models.filter((model) => accepted.some((pattern) => modelMatches(model.id, pattern)));
120
+ if (filtered.length) return { models: filtered, notice: '' };
121
+ const opus = models.filter((model) => /opus/i.test(model.id ?? ''));
122
+ return { models: opus, notice: '⚠ no compatible models returned — showing all Opus-class options' };
123
+ }
124
+
125
+ function searchable(model, query) {
126
+ const needle = query.toLowerCase();
127
+ return [model.id, model.name, model.display_name, model.description].some((value) => String(value ?? '').toLowerCase().includes(needle));
128
+ }
129
+
130
+ function readInput() {
131
+ return new Promise((resolve) => process.stdin.once('data', (chunk) => resolve(chunk.toString())));
132
+ }
133
+
134
+ async function pickAdvisorModel(models, options) {
135
+ const eligible = eligibleModels(models, options.mainModel);
136
+ const sorted = [...eligible.models].sort((a, b) => String(a.id).localeCompare(String(b.id)));
137
+ if (options.list) return { ok: true, models: sorted, notice: eligible.notice };
138
+ if (options.json || !process.stdin.isTTY || !process.stdout.isTTY) return { ok: true, models: sorted, notice: eligible.notice };
139
+
140
+ let query = '';
141
+ let index = 0;
142
+ let offset = 0;
143
+ const visible = 12;
144
+ const wasRaw = process.stdin.isRaw;
145
+ const redraw = () => {
146
+ const filtered = sorted.filter((model) => !query || searchable(model, query));
147
+ index = Math.min(index, Math.max(0, filtered.length - 1));
148
+ offset = Math.min(offset, Math.max(0, filtered.length - visible));
149
+ process.stdout.write('\x1b[2J\x1b[H');
150
+ process.stdout.write(chalk.bold(' Select Claude Code advisor model\n\n'));
151
+ if (eligible.notice) process.stdout.write(chalk.yellow(` ${eligible.notice}\n\n`));
152
+ process.stdout.write(` Search: ${query || chalk.dim('(type to filter)')}\n\n`);
153
+ for (let row = 0; row < Math.min(visible, filtered.length); row += 1) {
154
+ const model = filtered[offset + row];
155
+ process.stdout.write(`${offset + row === index ? chalk.cyan(' > ') : ' '}${model.id}${model.display_name ? chalk.dim(` — ${model.display_name}`) : ''}\n`);
156
+ }
157
+ if (!filtered.length) process.stdout.write(chalk.yellow(' No matching models\n'));
158
+ process.stdout.write(chalk.dim('\n ↑/↓ navigate enter select backspace edit esc cancel\n'));
159
+ };
160
+ const restore = () => {
161
+ if (process.stdin.isTTY) process.stdin.setRawMode(Boolean(wasRaw));
162
+ process.stdin.pause();
163
+ process.stdout.write('\x1b[2J\x1b[H');
164
+ };
165
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
166
+ process.stdin.resume();
167
+ try {
168
+ redraw();
169
+ while (true) {
170
+ const input = await readInput();
171
+ if (input === '' || input === '' || input === '') return { ok: false, error: 'cancelled' };
172
+ if (input === '\r' || input === '\n') {
173
+ const filtered = sorted.filter((model) => !query || searchable(model, query));
174
+ if (filtered[index]) return { ok: true, model: filtered[index].id, notice: eligible.notice };
175
+ } else if (input === '') query = query.slice(0, -1);
176
+ else if (input === '') index = Math.max(0, index - 1);
177
+ else if (input === '') index += 1;
178
+ else if (input.length === 1 && input >= ' ') query += input;
179
+ const filtered = sorted.filter((model) => !query || searchable(model, query));
180
+ index = Math.min(index, Math.max(0, filtered.length - 1));
181
+ if (index >= offset + visible) offset = index - visible + 1;
182
+ if (index < offset) offset = index;
183
+ redraw();
184
+ }
185
+ } finally {
186
+ restore();
187
+ }
188
+ }
189
+
190
+ export { pickAdvisorModel };
191
+
192
+ export function showAdvisorHelp() {
193
+ process.stdout.write(`\n bizar advisor — Configure Claude Code's advisor tool\n\n Usage:\n bizar advisor [pick|show|clear|validate|disable] [--list] [--json] [--yes]\n\n Subcommands:\n pick Search and select an advisor model (default)\n show Show the configured advisor and disabled state\n clear Remove advisorModel and the disable flag\n validate Validate the current main/advisor pairing\n disable Set CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1\n\n Options:\n --list Dump eligible model IDs without the interactive picker\n --json Emit machine-readable output\n --yes Skip confirmation prompts\n\n Settings: ~/.claude/settings.json or $CLAUDE_CONFIG_DIR/settings.json\n`);
194
+ }
195
+
196
+ function showAdvisor(settings, json) {
197
+ const state = {
198
+ advisorModel: settings.advisorModel ?? null,
199
+ disabled: settings.env?.CLAUDE_CODE_DISABLE_ADVISOR_TOOL === '1',
200
+ };
201
+ if (json) process.stdout.write(`${JSON.stringify(state)}\n`);
202
+ else if (state.disabled && state.advisorModel) process.stdout.write(chalk.yellow(` Advisor disabled; disabled state wins over ${state.advisorModel}. Run bizar advisor clear to reset.\n`));
203
+ else if (state.disabled) process.stdout.write(chalk.yellow(' Advisor disabled. Run bizar advisor clear to re-enable.\n'));
204
+ else if (state.advisorModel) process.stdout.write(` Advisor: ${state.advisorModel}\n`);
205
+ else process.stdout.write(' No advisor configured. Run bizar advisor pick.\n');
206
+ if (state.advisorModel && fableWarning(state.advisorModel) && !json) process.stdout.write(chalk.yellow(` ${fableWarning(state.advisorModel)}\n`));
207
+ return state;
208
+ }
209
+
210
+ export async function runAdvisor(args = []) {
211
+ const options = parseAdvisorArgs(args);
212
+ if (options.help) { showAdvisorHelp(); return { ok: true }; }
213
+ if (isAgentContext()) {
214
+ process.stderr.write(chalk.red('✗ bizar advisor is user-only and cannot run in agent context.\n'));
215
+ return { ok: false, error: 'agent-context' };
216
+ }
217
+ const path = settingsPath();
218
+ let settings;
219
+ try { settings = readSettings(path); } catch (error) {
220
+ process.stderr.write(chalk.red(`✗ Invalid JSON in ${path}: ${error.message}\n`));
221
+ return { ok: false, error: 'invalid-settings' };
222
+ }
223
+ const baseUrl = settings.env?.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
224
+ if (isUnsupportedProvider(baseUrl)) {
225
+ process.stderr.write(chalk.red(`✗ Advisor tool requires the Anthropic API. Detected provider: ${providerName(baseUrl)} (${baseUrl}). Disable this command on Bedrock/Vertex/Foundry.\n`));
226
+ return { ok: false, error: 'unsupported-provider' };
227
+ }
228
+ const subcommand = options.subcommand;
229
+ if (subcommand === 'show') return { ok: true, state: showAdvisor(settings, options.json) };
230
+ if (subcommand === 'clear' || subcommand === 'disable') {
231
+ const next = updateAdvisorSettings(settings, { action: subcommand });
232
+ writeSettings(path, next);
233
+ process.stdout.write(chalk.green(`✓ Advisor ${subcommand === 'clear' ? 'configuration cleared' : 'disabled'} in ${path}\n`));
234
+ return { ok: true, settings: next };
235
+ }
236
+ if (subcommand === 'validate') {
237
+ const main = resolveMainModel(settings);
238
+ const advisor = settings.advisorModel;
239
+ const result = main && advisor ? validateAdvisorPairing(main, advisor) : { verdict: 'missing-model', accepted: [] };
240
+ if (options.json) process.stdout.write(`${JSON.stringify({ main, advisor, ...result })}\n`);
241
+ else process.stdout.write(` Main: ${main ?? '(unknown)'}\n Advisor: ${advisor ?? '(unset)'}\n Verdict: ${result.verdict}\n`);
242
+ return { ok: result.verdict === 'ok' || result.verdict === 'unknown-main', result };
243
+ }
244
+ const models = await fetchAdvisorEndpointModels(baseUrl, settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY).catch((error) => {
245
+ process.stderr.write(chalk.red(`✗ Could not fetch advisor models: ${error.message}\n`));
246
+ return null;
247
+ });
248
+ if (!models) return { ok: false, error: 'models-fetch' };
249
+ const picked = await pickAdvisorModel(models, { ...options, mainModel: resolveMainModel(settings) });
250
+ if (!picked.ok) return picked;
251
+ if (options.list || options.json || picked.model === undefined) {
252
+ const output = options.json ? { models: picked.models.map((model) => model.id), notice: picked.notice } : picked.models.map((model) => model.id);
253
+ process.stdout.write(`${JSON.stringify(output, null, options.json ? 2 : 0)}\n`);
254
+ return { ok: true, models: picked.models, notice: picked.notice };
255
+ }
256
+ if (!options.yes) {
257
+ process.stdout.write(` Selected ${picked.model}. Write to settings? [y/N] `);
258
+ const answer = await readInput();
259
+ if (!/^y(?:es)?$/i.test(answer.trim())) return { ok: false, error: 'cancelled' };
260
+ }
261
+ const next = updateAdvisorSettings(settings, { action: 'pick', model: picked.model });
262
+ writeSettings(path, next);
263
+ process.stdout.write(chalk.green(`✓ Advisor set to ${picked.model}\n`));
264
+ const warning = fableWarning(picked.model);
265
+ if (warning) process.stdout.write(chalk.yellow(`${warning}\n`));
266
+ return { ok: true, model: picked.model, settings: next };
267
+ }
268
+
269
+ export async function run(name, args, isHelpRequest) {
270
+ if (name !== 'advisor') return false;
271
+ if (isHelpRequest) showAdvisorHelp();
272
+ else await runAdvisor(args);
273
+ return true;
274
+ }
@@ -1,6 +1,12 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { dirname, join, resolve as resolvePath } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { ensureOpenKanProject, installOpenKanPromise, OpenKanError, resolveOpenKanDashboard, runOpenKanOk } from '../openkan.mjs';
3
5
 
6
+ const HERE = dirname(fileURLToPath(import.meta.url));
7
+ const MIGRATE_TASKS = join(HERE, '..', '..', 'scripts', 'openkan', 'migrate-tasks-to-v2.mts');
8
+ const MIGRATE_BOARD = join(HERE, '..', '..', 'scripts', 'openkan', 'migrate-board-to-v2.mts');
9
+
4
10
  function print(result) {
5
11
  if (result.stdout) process.stdout.write(result.stdout);
6
12
  if (result.stderr) process.stderr.write(result.stderr);
@@ -18,6 +24,10 @@ Usage:
18
24
  ok plan <add|list|show|update> Manage plans and phases
19
25
  ok prd <add|list|show|update> Manage PRDs, goals, and milestones
20
26
  ok doctor Validate the .ok/ workspace
27
+ bizar openkan project clean [--apply|--all|--dry-run] Clean project workspace
28
+ bizar openkan board delete <id> Delete a board
29
+ bizar openkan migrate [--apply] [--tasks|--board] Migrate v1 tasks/board to v2 layout
30
+ (default: dry-run; --apply to apply)
21
31
  bizar openkan dashboard [args...] Forward to the OpenKan dashboard CLI
22
32
  (legacy openkan.mjs on pre-v0.5.0
23
33
  releases; ok serve on v0.5.0+ where
@@ -30,6 +40,25 @@ feature/progress files for live planning.
30
40
  `);
31
41
  }
32
42
 
43
+ /**
44
+ * Run one of the vendored OpenKan migration scripts. The scripts are
45
+ * `.mts` files that need `--experimental-strip-types` because they
46
+ * preserve the upstream TypeScript source verbatim from the v0.7.0
47
+ * tag. See `scripts/openkan/` for the vendored files.
48
+ */
49
+ function runMigrateScript(scriptPath, args) {
50
+ const resolved = resolvePath(scriptPath);
51
+ const result = spawnSync(process.execPath, ['--experimental-strip-types', resolved, ...args], {
52
+ cwd: process.cwd(),
53
+ encoding: 'utf8',
54
+ shell: false,
55
+ stdio: ['ignore', 'pipe', 'pipe'],
56
+ });
57
+ if (result.stdout) process.stdout.write(result.stdout);
58
+ if (result.stderr) process.stderr.write(result.stderr);
59
+ return result.status ?? 1;
60
+ }
61
+
33
62
  function runDashboard(args) {
34
63
  const launcher = resolveOpenKanDashboard();
35
64
  // OpenKan v0.5.0 dropped the legacy `openkan` dashboard launcher. The
@@ -74,6 +103,48 @@ export async function run(name, args, isHelpRequest) {
74
103
  print(runOpenKanOk([subcommand, ...rest]));
75
104
  return true;
76
105
  }
106
+ if (subcommand === 'project') {
107
+ // Forward project subcommands (e.g., project clean)
108
+ print(runOpenKanOk([subcommand, ...rest]));
109
+ return true;
110
+ }
111
+ if (subcommand === 'board') {
112
+ // Forward board subcommands (e.g., board delete)
113
+ print(runOpenKanOk([subcommand, ...rest]));
114
+ return true;
115
+ }
116
+ if (subcommand === 'migrate') {
117
+ // Migrate v1 tasks/board to the v2 layout that OpenKan 0.7.0 ships.
118
+ // Default is dry-run; --apply actually moves files. --tasks / --board
119
+ // narrow scope. The vendored scripts live under scripts/openkan/ and
120
+ // run via --experimental-strip-types because they're .mts.
121
+ const apply = rest.includes('--apply');
122
+ const tasksOnly = rest.includes('--tasks');
123
+ const boardOnly = rest.includes('--board');
124
+ if (!apply) {
125
+ process.stdout.write('Running in dry-run mode. Pass --apply to actually migrate.\n\n');
126
+ }
127
+ let exitCode = 0;
128
+ if (!boardOnly) {
129
+ process.stdout.write('=== Migrating tasks (.ok/tasks/<id>.json → <id>/task.json) ===\n');
130
+ const code = runMigrateScript(MIGRATE_TASKS, apply ? [] : ['--dry-run']);
131
+ if (code !== 0) exitCode = code;
132
+ }
133
+ if (!tasksOnly) {
134
+ process.stdout.write('\n=== Migrating board.json → per-task directories ===\n');
135
+ const code = runMigrateScript(MIGRATE_BOARD, apply ? [] : ['--dry-run']);
136
+ if (code !== 0) exitCode = code;
137
+ }
138
+ if (exitCode === 0) {
139
+ process.stdout.write(apply
140
+ ? '\n✓ Migration complete.\n'
141
+ : '\n✓ Dry-run complete. Re-run with --apply to execute.\n');
142
+ } else {
143
+ process.stderr.write('\n✗ Migration completed with errors. See output above.\n');
144
+ }
145
+ process.exitCode = exitCode;
146
+ return true;
147
+ }
77
148
  if (subcommand === 'dashboard') { runDashboard(rest); return true; }
78
149
  help();
79
150
  process.exitCode = 2;