@polderlabs/bizar 3.14.1 → 3.15.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
@@ -84,7 +84,7 @@ function showHelp() {
84
84
  plan <subcommand> Manage visual plans
85
85
  graph Per-project knowledge graph (powered by graphify)
86
86
  test-gate Detect & run the project's test suite
87
- update Update opencode, bizar, and/or bizar-plugin
87
+ update Auto-update everything (opencode + bizar + dash + plugin)
88
88
  service Manage the background service daemon
89
89
  bg <subcommand> Manage background agents (list/view/kill/logs)
90
90
  dash <subcommand> Manage the dashboard (start/stop/status/tui)
@@ -182,13 +182,12 @@ function showInstallHelp() {
182
182
 
183
183
  function showUpdateHelp() {
184
184
  console.log(`
185
- bizar update — Update opencode, bizar, and/or bizar-plugin
185
+ bizar update — Update opencode, bizar, bizar-dash, and/or bizar-plugin
186
186
 
187
187
  Usage:
188
- bizar update Interactive prompt for components
189
- bizar update --all Update every component
188
+ bizar update Update EVERYTHING (default; auto-kills + restarts)
189
+ bizar update --pick Interactive picker (legacy per-component UI)
190
190
  bizar update opencode bizar dash Update specific components
191
- bizar update --all --yes Update everything + auto-kill running instances
192
191
  bizar update --no-restart Don't auto-restart the dashboard after update
193
192
  bizar update --dry-run Print what would happen, change nothing
194
193
  bizar update --help Show this help
@@ -199,13 +198,15 @@ function showUpdateHelp() {
199
198
  dash @polderlabs/bizar-dash (web dashboard + TUI)
200
199
  plugin @polderlabs/bizar-plugin (opencode plugin)
201
200
 
202
- Behavior:
201
+ Behavior (v3.15.0+):
202
+ • Default = automatic. Updates all 4 components without prompting.
203
+ Any missing package is auto-installed so a broken install gets
204
+ repaired in one shot.
203
205
  • Detects running Bizar instances (background service daemon, web
204
206
  dashboard) by reading ~/.config/bizar/{service,dashboard}.pid and
205
- cleaning up any stale or empty PID files.
206
- Warns the user explicitly before killing each instance. Pass
207
- --yes / -y / --force to skip the confirmation (required for
208
- non-interactive shells).
207
+ cleans up any stale or empty PID files.
208
+ Auto-kills running instances with a brief notice (use --pick to
209
+ be prompted first instead).
209
210
  • Sends SIGTERM, waits up to 5s, escalates to SIGKILL if needed.
210
211
  • Re-runs the install script so the deployed plugin source matches
211
212
  the just-upgraded npm version (avoids the version-skew trap).
@@ -214,15 +215,12 @@ function showUpdateHelp() {
214
215
  (skipped with --no-restart).
215
216
  • Runs \`bizar doctor\` after a successful update to catch config
216
217
  regressions before opencode tries to start.
217
- • If ~/.config/opencode/plugins/bizar is a dev symlink (created
218
- by \`bizar dev-link\`), the copy step is skipped to preserve the
219
- link. Use \`bizar dev-unlink\` first, or pass --force.
220
218
 
221
219
  Examples:
222
- bizar update --all --yes Headless full update + restart
223
- bizar update dash Update only the dashboard
220
+ bizar update Full auto-update (recommended)
221
+ bizar update --dry-run Preview what would change
222
+ bizar update --pick Pick specific components
224
223
  bizar update plugin --no-restart Plugin-only, leave dashboard alone
225
- bizar update --all --dry-run See what would change without doing it
226
224
  `);
227
225
  }
228
226
 
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli/graph-build-from-cache.mjs
4
+ *
5
+ * One-off script for environments where graphify's semantic-extraction
6
+ * step fails (e.g. no LLM API key + remaining non-code docs that the
7
+ * `.graphifyignore` can't filter) but the AST cache is intact.
8
+ *
9
+ * graphify's pipeline has two stages:
10
+ * 1. AST extraction (local, tree-sitter) — writes per-file JSON to
11
+ * `.bizar/graph/cache/ast/v0.8.46/<hash>.json` regardless of LLM status.
12
+ * 2. Semantic extraction (LLM, optional) — merges per-file nodes into a
13
+ * project-wide graph.
14
+ *
15
+ * If stage 2 aborts, `graph.json` is never written and the dashboard has
16
+ * nothing to render. This script reconstructs `graph.json` directly from
17
+ * the per-file AST cache, then calls `graphify cluster-only` to generate
18
+ * `graph.html` + `GRAPH_REPORT.md`.
19
+ *
20
+ * Usage:
21
+ * node cli/graph-build-from-cache.mjs # use .bizar/graph/
22
+ * node cli/graph-build-from-cache.mjs /tmp/other/graph # custom dir
23
+ */
24
+
25
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
26
+ import { join, resolve } from 'node:path';
27
+
28
+ const graphDirArg = process.argv[2] || '.bizar/graph';
29
+ const graphDir = resolve(graphDirArg);
30
+
31
+ // The cache version directory inside graphify's cache layout. Bumping the
32
+ // graphify version moves the cache; we discover the actual subdir at
33
+ // runtime so we don't hardcode "v0.8.46".
34
+ const cacheAstRoot = join(graphDir, 'cache', 'ast');
35
+
36
+ function findCacheDir() {
37
+ if (!existsSync(cacheAstRoot)) {
38
+ throw new Error(`No AST cache found at ${cacheAstRoot}. Run \`bizar graph build\` first to populate it.`);
39
+ }
40
+ const entries = readdirSync(cacheAstRoot);
41
+ if (entries.length === 0) {
42
+ throw new Error(`AST cache directory ${cacheAstRoot} is empty.`);
43
+ }
44
+ // graphify uses one version directory like v0.8.46. Pick the first.
45
+ return join(cacheAstRoot, entries[0]);
46
+ }
47
+
48
+ function buildGraphFromCache(cacheDir) {
49
+ const files = readdirSync(cacheDir).filter((f) => f.endsWith('.json'));
50
+ console.log(` Reading ${files.length} AST cache files from ${cacheDir}...`);
51
+
52
+ const nodesById = new Map();
53
+ const edges = [];
54
+ const rawCalls = [];
55
+
56
+ let fileIdx = 0;
57
+ for (const f of files) {
58
+ fileIdx++;
59
+ const raw = JSON.parse(readFileSync(join(cacheDir, f), 'utf8'));
60
+ for (const n of raw.nodes || []) {
61
+ // Later files win on duplicate id (consistent with graphify's
62
+ // dedup behavior — first write keeps the canonical location).
63
+ if (!nodesById.has(n.id)) nodesById.set(n.id, n);
64
+ }
65
+ for (const e of raw.edges || []) edges.push(e);
66
+ for (const rc of raw.raw_calls || []) rawCalls.push(rc);
67
+ if (fileIdx % 50 === 0) {
68
+ console.log(` merged ${fileIdx}/${files.length} files`);
69
+ }
70
+ }
71
+
72
+ // Re-key node.community so it matches the dashboard's expected schema.
73
+ // The AST cache uses `community` as a numeric cluster id; we keep it as-is.
74
+ const nodes = Array.from(nodesById.values());
75
+
76
+ const graph = {
77
+ directed: false,
78
+ multigraph: false,
79
+ graph: {},
80
+ nodes,
81
+ links: edges,
82
+ };
83
+
84
+ console.log(` Built graph: ${nodes.length} nodes, ${edges.length} edges (raw_calls=${rawCalls.length})`);
85
+ return { graph, rawCalls };
86
+ }
87
+
88
+ function writeOutputs(graphDir, graph) {
89
+ // graph.json — NetworkX node-link format
90
+ const graphJsonPath = join(graphDir, 'graph.json');
91
+ writeFileSync(graphJsonPath, JSON.stringify(graph, null, 2), 'utf8');
92
+ console.log(` ✓ Wrote ${graphJsonPath}`);
93
+
94
+ // .graphify_analysis.json — graphify's secondary output. cluster-only
95
+ // doesn't strictly require this, but writing it keeps the directory
96
+ // shape consistent with what `graphify .` would have produced.
97
+ const analysis = {
98
+ schema_version: 1,
99
+ generated_at: new Date().toISOString(),
100
+ clusters: [],
101
+ };
102
+ const analysisPath = join(graphDir, '.graphify_analysis.json');
103
+ writeFileSync(analysisPath, JSON.stringify(analysis, null, 2), 'utf8');
104
+ console.log(` ✓ Wrote ${analysisPath}`);
105
+ }
106
+
107
+ function main() {
108
+ console.log(` Graph dir: ${graphDir}`);
109
+ if (!existsSync(graphDir)) {
110
+ throw new Error(`Graph dir does not exist: ${graphDir}`);
111
+ }
112
+ const cacheDir = findCacheDir();
113
+ const { graph } = buildGraphFromCache(cacheDir);
114
+ writeOutputs(graphDir, graph);
115
+ console.log('');
116
+ console.log(' Next step: run `bizar graph cluster-only` to generate graph.html.');
117
+ }
118
+
119
+ try {
120
+ main();
121
+ } catch (err) {
122
+ console.error(`\n Error: ${err.message}\n`);
123
+ process.exit(1);
124
+ }
package/cli/graph.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import chalk from 'chalk';
9
9
  import { spawnSync } from 'node:child_process';
10
- import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
10
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, copyFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
 
13
13
  // ── Constants ─────────────────────────────────────────────────────────────────
@@ -15,6 +15,82 @@ import { join } from 'node:path';
15
15
  export const GRAPH_DIR = '.bizar/graph';
16
16
  export const GRAPHIFY_OUT_ENV = 'GRAPHIFY_OUT';
17
17
 
18
+ /**
19
+ * Env vars graphify recognises for semantic extraction of docs/papers/images.
20
+ * If NONE of these are set, graphify refuses to run on a corpus that contains
21
+ * any non-code file ("A code-only corpus needs no key."). We auto-detect this
22
+ * and write a comprehensive .graphifyignore so a fresh checkout can build
23
+ * the graph offline with no LLM key.
24
+ */
25
+ const GRAPHIFY_LLM_KEY_ENVS = [
26
+ 'GEMINI_API_KEY', 'GOOGLE_API_KEY',
27
+ 'MOONSHOT_API_KEY',
28
+ 'ANTHROPIC_API_KEY',
29
+ 'OPENAI_API_KEY',
30
+ 'DEEPSEEK_API_KEY',
31
+ 'AZURE_OPENAI_API_KEY',
32
+ ];
33
+
34
+ /**
35
+ * Minimal .graphifyignore that restricts graphify to code-only extraction.
36
+ * Written automatically by `bizar graph build` when no LLM key is set.
37
+ * The user can override this file with their own .graphifyignore — we
38
+ * never overwrite a pre-existing one.
39
+ */
40
+ const CODE_ONLY_GRAPHIFYIGNORE = [
41
+ '# Auto-generated by `bizar graph build` — code-only mode.',
42
+ '# Bizar detected no LLM key, so docs/papers/images cannot be semantically',
43
+ '# extracted. We restrict extraction to code files (processed locally via',
44
+ "# tree-sitter — no API key needed). Delete this file and re-run once you've",
45
+ '# set one of: GEMINI_API_KEY, GOOGLE_API_KEY, MOONSHOT_API_KEY,',
46
+ '# ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, AZURE_OPENAI_API_KEY.',
47
+ '',
48
+ '# Heavy / vendored dirs to skip entirely',
49
+ 'node_modules/',
50
+ '.git/',
51
+ '.bizar/',
52
+ '.agents/',
53
+ 'dist/',
54
+ 'build/',
55
+ 'coverage/',
56
+ '',
57
+ '# Agent system prompts — they\'re Markdown but contain YAML frontmatter',
58
+ '# + system instructions, not project documentation. They\'re noise for',
59
+ '# a structural graph and would balloon the doc count to ~1100.',
60
+ 'config/agents/',
61
+ '',
62
+ '# Non-code file types — graphify would need an LLM for these',
63
+ '*.md',
64
+ '*.mdx',
65
+ '*.qmd',
66
+ '*.html',
67
+ '*.txt',
68
+ '*.rst',
69
+ '*.pdf',
70
+ '*.png',
71
+ '*.jpg',
72
+ '*.jpeg',
73
+ '*.webp',
74
+ '*.gif',
75
+ '*.mp4',
76
+ '*.mov',
77
+ '*.mp3',
78
+ '*.wav',
79
+ '',
80
+ ].join('\n');
81
+
82
+ /**
83
+ * Detect whether any graphify-recognised LLM API key is set in the
84
+ * current process env. Used by `cmdBuild` to decide whether to write
85
+ * the code-only .graphifyignore.
86
+ */
87
+ function hasLlmKey(env = process.env) {
88
+ return GRAPHIFY_LLM_KEY_ENVS.some((k) => {
89
+ const v = env[k];
90
+ return typeof v === 'string' && v.trim().length > 0;
91
+ });
92
+ }
93
+
18
94
  // ── Python detection ───────────────────────────────────────────────────────────
19
95
 
20
96
  /**
@@ -190,15 +266,176 @@ function runGraphify(python, subArgs, extraEnv = {}) {
190
266
  // ── Subcommand handlers ────────────────────────────────────────────────────────
191
267
 
192
268
  async function cmdBuild(python) {
269
+ // If no LLM key is set, fall back to a code-only build by writing a
270
+ // comprehensive .graphifyignore. We never overwrite a user-provided
271
+ // .graphifyignore — only create the file when it doesn't exist.
272
+ let llmKeyPresent = hasLlmKey();
273
+ if (!llmKeyPresent) {
274
+ const ignorePath = '.graphifyignore';
275
+ if (!existsSync(ignorePath)) {
276
+ writeFileSync(ignorePath, CODE_ONLY_GRAPHIFYIGNORE, 'utf8');
277
+ console.log(chalk.dim(' No LLM API key detected — switching to code-only build.'));
278
+ console.log(chalk.dim(` Wrote ${ignorePath} restricting extraction to code files.`));
279
+ console.log(chalk.dim(' Set GEMINI_API_KEY / OPENAI_API_KEY / etc. and re-run for full docs extraction.\n'));
280
+ } else {
281
+ console.log(chalk.dim(' No LLM API key detected — using existing .graphifyignore for code-only build.\n'));
282
+ }
283
+ }
284
+
193
285
  console.log(chalk.dim(' Running full graphify pipeline...'));
194
- const code = runGraphify(python, ['.']);
286
+ let code = runGraphify(python, ['.']);
287
+
288
+ // Offline code-only fallback — when no LLM key is set, graphify's
289
+ // pre-flight check rejects the build before it can write any output.
290
+ // Even with a dummy key set, the semantic step fails for any non-code
291
+ // docs that escaped the .graphifyignore filter, and graphify refuses
292
+ // to write graph.json. To unblock this case we:
293
+ // 1. Run graphify with a dummy ANTHROPIC_API_KEY so the AST cache
294
+ // gets populated (semantic extraction will fail, that's fine).
295
+ // 2. Reconstruct graph.json from the per-file AST cache.
296
+ // 3. Run cluster-only to generate graph.html + GRAPH_REPORT.md.
297
+ if (code !== 0 && !llmKeyPresent) {
298
+ const cacheAstRoot = join(GRAPH_DIR, 'cache', 'ast');
299
+ const cacheIsPopulated = existsSync(cacheAstRoot) && (
300
+ // graphify creates a versioned subdir on first extract.
301
+ // Check if any version dir contains JSON files.
302
+ (await import('node:fs/promises'))
303
+ .readdir(cacheAstRoot, { withFileTypes: true })
304
+ .then((entries) =>
305
+ entries.some(async (e) => {
306
+ if (!e.isDirectory()) return false;
307
+ const files = await (await import('node:fs/promises')).readdir(join(cacheAstRoot, e.name));
308
+ return files.some((f) => f.endsWith('.json'));
309
+ }),
310
+ )
311
+ .catch(() => false)
312
+ );
313
+
314
+ if (!cacheIsPopulated) {
315
+ console.log(chalk.yellow(' ⚠ No AST cache yet. Running graphify with a dummy key to populate the cache (semantic extraction will fail; that\'s OK)...'));
316
+ // Set a dummy key so graphify gets past its pre-flight check and
317
+ // populates the AST cache. We invoke the graphify binary directly
318
+ // (the user's python3 -m graphify may not have graphify installed;
319
+ // uv-tool installs put a shim at ~/.local/bin/graphify).
320
+ const dummyRes = spawnSync('graphify', ['.'], {
321
+ stdio: 'inherit',
322
+ env: { ...process.env, ANTHROPIC_API_KEY: 'sk-bizar-graph-dummy', GRAPHIFY_OUT: GRAPH_DIR },
323
+ timeout: 0,
324
+ });
325
+ void dummyRes; // exit code doesn't matter — we want the cache, not the merge
326
+ }
327
+
328
+ const cacheStillEmpty = !(await (async () => {
329
+ try {
330
+ const e = await (await import('node:fs/promises')).readdir(cacheAstRoot, { withFileTypes: true });
331
+ for (const d of e) {
332
+ if (!d.isDirectory()) continue;
333
+ const f = await (await import('node:fs/promises')).readdir(join(cacheAstRoot, d.name));
334
+ if (f.some((x) => x.endsWith('.json'))) return true;
335
+ }
336
+ return false;
337
+ } catch {
338
+ return false;
339
+ }
340
+ })());
341
+
342
+ if (!cacheStillEmpty) {
343
+ console.log(chalk.yellow(' ⚠ graphify exited non-zero. Falling back to AST-cache build...'));
344
+ const fb = runCacheFallbackBuild();
345
+ if (fb === 0) code = 0;
346
+ }
347
+ }
348
+
195
349
  if (code === 0) {
196
350
  ensureGitignore(GRAPH_DIR);
351
+ // After extract, run cluster-only to write graph.html + GRAPH_REPORT.md
352
+ // (extract only writes graph.json; cluster-only adds viz + report).
353
+ const graphJson = join(GRAPH_DIR, 'graph.json');
354
+ if (existsSync(graphJson)) {
355
+ console.log(chalk.dim(' Generating graph.html + GRAPH_REPORT.md via cluster-only...'));
356
+ // Pass "." as the cluster-only path so it writes graph.html +
357
+ // GRAPH_REPORT.md to the current directory's graphify-out/.
358
+ // (Passing `.bizar/graph` directly nests the output as
359
+ // `.bizar/graph/.bizar/graph/...` which is wrong.) The explicit
360
+ // --graph overrides the auto-discovery to find graph.json.
361
+ const clusterArgs = ['cluster-only', '.', '--graph', graphJson];
362
+ const clusterCode = runGraphify(python, clusterArgs);
363
+ if (clusterCode !== 0) {
364
+ console.log(chalk.yellow(` ⚠ cluster-only returned ${clusterCode} — graph.json was built but graph.html may be stale.`));
365
+ } else {
366
+ // cluster-only writes to ./graphify-out/ (relative to CWD).
367
+ // Promote the artifacts up to .bizar/graph/ paths the dashboard
368
+ // serves directly.
369
+ const out = join(GRAPH_DIR, 'graphify-out');
370
+ if (!existsSync(out)) {
371
+ // Fallback: graphify may have written to ./graphify-out/ at CWD.
372
+ const cwdOut = join(process.cwd(), 'graphify-out');
373
+ if (existsSync(cwdOut)) {
374
+ promoteFromDir(cwdOut, GRAPH_DIR);
375
+ }
376
+ } else {
377
+ promoteClusterOutputs(GRAPH_DIR);
378
+ }
379
+ }
380
+ }
197
381
  console.log(chalk.green(`\n ✓ Graph built → ${GRAPH_DIR}/\n`));
198
382
  }
199
383
  return code;
200
384
  }
201
385
 
386
+ /**
387
+ * Reconstruct graph.json from graphify's per-file AST cache and run
388
+ * cluster-only to produce graph.html + GRAPH_REPORT.md. Used when the
389
+ * full graphify pipeline fails because some docs need an LLM that we
390
+ * don't have, but the AST cache for code-only files was successfully
391
+ * extracted.
392
+ *
393
+ * Returns the exit code of the overall sequence (0 on success).
394
+ */
395
+ function runCacheFallbackBuild() {
396
+ // Step 1: merge the per-file AST cache into a single graph.json.
397
+ const scriptPath = new URL(import.meta.url).pathname;
398
+ // cli/graph.mjs sits next to cli/graph-build-from-cache.mjs.
399
+ const cacheScript = scriptPath.replace(/graph\.mjs$/, 'graph-build-from-cache.mjs');
400
+ if (!existsSync(cacheScript)) {
401
+ console.log(chalk.red(` ✗ Cache fallback script not found at ${cacheScript}`));
402
+ return 1;
403
+ }
404
+ const buildRes = spawnSync(process.execPath, [cacheScript, GRAPH_DIR], {
405
+ stdio: 'inherit',
406
+ });
407
+ if (buildRes.status !== 0) {
408
+ return buildRes.status ?? 1;
409
+ }
410
+ // Step 2: cluster-only regenerates graph.html from the merged graph.json.
411
+ return 0;
412
+ }
413
+
414
+ /**
415
+ * Copy the named files from `srcDir` up to `dstDir`. Used to hoist
416
+ * cluster-only's graph.html / GRAPH_REPORT.md / graph.json up to the
417
+ * canonical .bizar/graph/ paths the dashboard serves.
418
+ */
419
+ function promoteFromDir(srcDir, dstDir) {
420
+ for (const name of ['graph.html', 'GRAPH_REPORT.md', 'graph.json']) {
421
+ const src = join(srcDir, name);
422
+ const dst = join(dstDir, name);
423
+ if (existsSync(src)) {
424
+ copyFileSync(src, dst);
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * cluster-only writes its outputs to a nested graphify-out/ directory
431
+ * by default. This hoists them up to the standard paths the dashboard
432
+ * serves (.bizar/graph/graph.html + .bizar/graph/GRAPH_REPORT.md).
433
+ */
434
+ function promoteClusterOutputs(graphDir) {
435
+ const out = join(graphDir, 'graphify-out');
436
+ if (existsSync(out)) promoteFromDir(out, graphDir);
437
+ }
438
+
202
439
  async function cmdUpdate(python) {
203
440
  console.log(chalk.dim(' Running incremental graph update...'));
204
441
  return runGraphify(python, ['.', '--update']);
package/cli/update.mjs CHANGED
@@ -475,6 +475,22 @@ async function promptForUpdates(forceAll) {
475
475
  // Main entry point
476
476
  // ---------------------------------------------------------------------------
477
477
 
478
+ /**
479
+ * Default behavior policy for `bizar update`:
480
+ * - Update EVERYTHING (opencode + bizar + dash + plugin) automatically.
481
+ * - Auto-install any missing component without asking.
482
+ * - Kill running instances without confirmation (with a brief notice).
483
+ * - Re-run the install script to refresh the on-disk plugin.
484
+ * - Restart the dashboard if it was running.
485
+ *
486
+ * The `--pick` / `-p` flag opts into the legacy per-component picker
487
+ * (checkbox UI) for users who want to update only some components.
488
+ *
489
+ * The `--dry-run` flag previews what would happen without touching
490
+ * anything.
491
+ *
492
+ * The `--no-restart` flag skips the dashboard restart step.
493
+ */
478
494
  export async function runUpdate(subargs = []) {
479
495
  console.log(chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE ᚦ\n'));
480
496
 
@@ -483,6 +499,11 @@ export async function runUpdate(subargs = []) {
483
499
  const restartAfter = !subargs.includes('--no-restart');
484
500
  const forceAll = subargs.includes('--all');
485
501
  const dryRun = subargs.includes('--dry-run');
502
+ // Opt-in interactive picker. Without `--pick`, we update everything
503
+ // automatically. This matches what the user actually wants 95% of
504
+ // the time ("update my install") and removes a prompt that
505
+ // interrupted the flow.
506
+ const interactivePick = subargs.includes('--pick') || subargs.includes('-p');
486
507
 
487
508
  if (dryRun) {
488
509
  console.log(
@@ -503,13 +524,16 @@ export async function runUpdate(subargs = []) {
503
524
  chalk.dim(` [dry-run] would stop running instances: ${labels.join(', ')}`),
504
525
  );
505
526
  } else {
506
- const ok = await confirmKill(instances, { assumeYes });
507
- if (!ok) {
508
- console.log(chalk.yellow('\n Update cancelled instances still running.'));
509
- console.log(chalk.dim(' Stop them with `bizar service stop` and `bizar dashboard stop`, then retry.'));
510
- process.exit(1);
511
- }
512
- console.log(chalk.cyan('\n Stopping running instances...'));
527
+ // In automatic mode we still print what we're about to kill
528
+ // but don't prompt — the operator asked for a full update and
529
+ // these processes would block the npm replace anyway.
530
+ const labels = [];
531
+ if (instances.service) labels.push(instances.service.label);
532
+ if (instances.dashboard) labels.push(instances.dashboard.label);
533
+ console.log(chalk.yellow(` Stopping running instances: ${labels.join(', ')}`));
534
+ console.log(
535
+ chalk.dim(' (use `--pick` to be prompted before killing in future)'),
536
+ );
513
537
  const kills = await killInstances(instances);
514
538
  for (const k of kills) {
515
539
  const marker = k.ok ? chalk.green('✓') : chalk.red('✗');
@@ -552,31 +576,23 @@ export async function runUpdate(subargs = []) {
552
576
  }
553
577
  console.log('');
554
578
 
555
- // 3. Decide what to update.
579
+ // 3. Decide what to update. Default = everything. Any missing package
580
+ // is automatically included so a partial install gets completed.
556
581
  let selected;
557
- if (forceAll || assumeYes || dryRun) {
558
- // dryRun defaults to showing everything (the whole point is to see
559
- // what *would* change across the board). Pass a positional list to
560
- // narrow the preview.
561
- selected = new Set(COMPONENTS);
562
- // If the user passed positional component names alongside --dry-run,
563
- // narrow the selection to those.
564
- const positional = subargs.filter((a) => !a.startsWith('-'));
565
- if (positional.length > 0) {
566
- const valid = new Set(COMPONENTS);
567
- const narrowed = positional.filter((a) => valid.has(a));
568
- if (narrowed.length > 0) selected = new Set(narrowed);
569
- }
570
- } else if (subargs.length === 0) {
582
+ if (interactivePick && !dryRun && !forceAll && !assumeYes) {
571
583
  selected = await promptForUpdates(false);
572
584
  } else {
573
- const valid = new Set(COMPONENTS);
574
- selected = new Set(subargs.filter((a) => !a.startsWith('-') && valid.has(a)));
575
- if (selected.size === 0) {
576
- console.log(chalk.yellow(` No valid components selected from: ${subargs.join(' ')}`));
577
- console.log(chalk.dim(' Valid components: opencode, bizar, dash, plugin'));
578
- console.log(chalk.dim(' Run `bizar update --help` for usage.'));
579
- process.exit(1);
585
+ // Auto-select: all components + any missing one (so a broken install
586
+ // gets repaired automatically).
587
+ selected = new Set(COMPONENTS);
588
+ for (const k of COMPONENTS) {
589
+ if (cur[k] === null && latest[k] !== null) selected.add(k);
590
+ }
591
+ if (interactivePick) {
592
+ // --pick + --dry-run / --all / --yes → still update everything
593
+ // but make the auto-selection visible so the dry-run output is
594
+ // informative.
595
+ console.log(chalk.dim(` Auto-selecting all components (--pick ignored due to flags).`));
580
596
  }
581
597
  }
582
598
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "3.14.1",
3
+ "version": "3.15.1",
4
4
  "description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness.",
5
5
  "type": "module",
6
6
  "bin": {