@polderlabs/bizar 3.20.10 → 3.20.12

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
@@ -150,14 +150,22 @@ function showExportHelp() {
150
150
 
151
151
  function showInstallHelp() {
152
152
  console.log(`
153
- bizar install — Run the interactive installer
153
+ bizar install — Run the canonical BizarHarness installer
154
154
 
155
155
  Usage:
156
156
  bizar install
157
157
 
158
158
  Description:
159
- Installs agents, rules, commands, plugin support, RTK, Semble,
160
- and the Skills CLI.
159
+ v3.20.11+ thin wrapper around ./install.sh. The bash script
160
+ auto-installs every system dep (uv, python3.12, chrome-headless-shell
161
+ + runtime libs, jq), installs browser-harness via uv, syncs agents /
162
+ commands / hooks / skills into ~/.config/opencode/, configures
163
+ opencode.json + the Bizar plugin, and prints a single status banner.
164
+
165
+ No API key collection, no interactive prompts, no opencode restart —
166
+ the installer is safe to re-run anytime.
167
+
168
+ Run from a fresh git clone instead: \`cd BizarHarness && ./install.sh\`
161
169
  `);
162
170
  }
163
171
 
package/cli/doctor.mjs CHANGED
@@ -25,7 +25,28 @@ import { homedir } from 'node:os';
25
25
  import { join } from 'node:path';
26
26
  import { opencodeConfigDir, opencodeAgentsDir } from './utils.mjs';
27
27
 
28
- const REQUIRED_AGENTS = ['odin.md', 'quick.md', 'thor.md', 'tyr.md'];
28
+ // v3.20.11: list every agent the install script is expected to deploy.
29
+ // Adding a new agent to `config/agents/` without adding it here causes
30
+ // doctor to silently under-count ("all 4 core agents present" when there
31
+ // are actually 14). The list mirrors cli/install.mjs AGENT_FILES plus
32
+ // `browser-harness.md` (added in v3.20.7) and `_shared/AGENT_BASELINE.md`
33
+ // is intentionally excluded (it's a skill, not an agent).
34
+ const REQUIRED_AGENTS = [
35
+ 'odin.md',
36
+ 'vor.md',
37
+ 'frigg.md',
38
+ 'quick.md',
39
+ 'mimir.md',
40
+ 'heimdall.md',
41
+ 'hermod.md',
42
+ 'thor.md',
43
+ 'baldr.md',
44
+ 'tyr.md',
45
+ 'vidarr.md',
46
+ 'forseti.md',
47
+ 'semble-search.md',
48
+ 'browser-harness.md',
49
+ ];
29
50
 
30
51
  // ── helpers ─────────────────────────────────────────────────────────────────
31
52
 
@@ -281,16 +281,22 @@ describe('runDoctor() with fixture HOME', () => {
281
281
 
282
282
  test('agent-files-installed fails when core agents missing', async () => {
283
283
  writeOpencodeConfig({});
284
- writeAgents('odin.md'); // missing quick, thor, tyr
284
+ writeAgents('odin.md'); // missing the other 13
285
285
  const result = await runDoctor({ silent: true });
286
286
  const r = findCheck(result, 'agent-files-installed');
287
287
  assert.equal(r.ok, false);
288
288
  assert.match(r.message, /missing/);
289
289
  });
290
290
 
291
- test('agent-files-installed passes when all core agents present', async () => {
291
+ test('agent-files-installed passes when all 14 agents present', async () => {
292
292
  writeOpencodeConfig({});
293
- writeAgents('odin.md', 'quick.md', 'thor.md', 'tyr.md');
293
+ // v3.20.11: doctor now expects all 14 agents (was 4 in v3.20.10).
294
+ writeAgents(
295
+ 'odin.md', 'vor.md', 'frigg.md', 'quick.md',
296
+ 'mimir.md', 'heimdall.md', 'hermod.md', 'thor.md', 'baldr.md',
297
+ 'tyr.md', 'vidarr.md', 'forseti.md',
298
+ 'semble-search.md', 'browser-harness.md',
299
+ );
294
300
  const result = await runDoctor({ silent: true });
295
301
  const r = findCheck(result, 'agent-files-installed');
296
302
  assert.equal(r.ok, true, r.message);
package/cli/install.mjs CHANGED
@@ -206,231 +206,62 @@ export async function installPluginFromGlobal(opts = {}) {
206
206
  return true;
207
207
  }
208
208
 
209
+ /**
210
+ * runInstaller — v3.20.11 thin wrapper.
211
+ *
212
+ * As of v3.20.11, `bizar install` is a thin wrapper around the canonical
213
+ * `install.sh` script at the repo root. The bash script handles all the
214
+ * heavy lifting: system-dep installation (uv, python3.12, chrome-headless-shell
215
+ * + runtime libs, jq), browser-harness via uv, Chrome lifecycle, agent /
216
+ * command / hook / skill sync, opencode.json merging, install-state.
217
+ *
218
+ * Why a thin wrapper instead of the old TUI:
219
+ * - One source of truth. The bash script is also what `git clone` users
220
+ * run (`./install.sh`), what `bizar update` re-runs, and what the npm
221
+ * postinstall hook invokes. Keeping a parallel Node implementation
222
+ * guarantees the two will diverge.
223
+ * - No interactive prompts. The TUI asked for API keys, restart
224
+ * confirmation, and component selection — none of which belong in a
225
+ * `npm i -g @polderlabs/bizar` install (no TTY, secrets stay local).
226
+ * - Cross-platform. The bash script already detects Windows and falls
227
+ * back to the npm-based path; re-implementing that in Node is busywork.
228
+ *
229
+ * Returns the install.sh exit code (0 = success, 1 = partial failure).
230
+ * On platforms where bash isn't available (rare — Windows without WSL),
231
+ * falls back to running the npm-based path directly.
232
+ */
209
233
  export async function runInstaller() {
210
- showBanner();
211
-
212
- // ── Detect opencode ──
213
- sectionHeading('Pre-flight');
214
- const env = await detectOpenCode();
215
-
216
- if (!env.exists) {
217
- console.log(chalk.yellow(' ⚠ opencode config directory not found.'));
218
- console.log(chalk.dim(' The installer will create it at:'));
219
- console.log(chalk.dim(` ${env.configDir}`));
220
- } else {
221
- console.log(chalk.green(` ✓ opencode detected at ${env.configDir}`));
222
- if (env.version) console.log(chalk.dim(` version ${env.version}`));
223
- }
224
-
225
- const rtkInstalled = await detectRtk();
226
- if (rtkInstalled) {
227
- console.log(chalk.green(' ✓ RTK detected (token optimizer)'));
228
- } else {
229
- console.log(chalk.yellow(' ○ RTK not detected — will install'));
230
- }
231
-
232
- const sembleInstalled = await detectSemble();
233
- if (sembleInstalled) {
234
- console.log(chalk.green(' ✓ Semble detected (code search)'));
235
- } else {
236
- console.log(chalk.yellow(' ○ Semble not detected — will install'));
237
- }
238
-
239
- const skillsCliInstalled = await detectSkillsCli();
240
- if (skillsCliInstalled) {
241
- console.log(chalk.green(' ✓ Skills CLI detected (skill discovery)'));
242
- } else {
243
- console.log(chalk.yellow(' ○ Skills CLI not detected — will install'));
244
- }
245
- console.log();
246
-
247
- // ── Step 1: Component selection ──
248
- sectionHeading('Component Selection');
249
- const components = await promptComponents();
250
-
251
- // ── Step 2: Agent selection (if agents component chosen) ──
252
- let selectedAgents = [];
253
- if (components.includes('agents')) {
254
- sectionHeading('Agent Selection');
255
- selectedAgents = await promptAgents();
256
- }
257
-
258
- // ── Step 3: Install mode ──
259
- sectionHeading('Installation Mode');
260
- const mode = await promptInstallMode();
261
-
262
- // ── Step 4: Skill packs (via skills.sh) ──
263
- sectionHeading('Skills from skills.sh');
264
- const skillPacks = await promptSkillPacks();
265
-
266
- // ── Step 5: Build summary & confirm ──
267
- const summary = buildSummary(components, selectedAgents, env.configDir, skillPacks);
268
- showPantheon();
269
- console.log();
270
- console.log(chalk.dim(' Summary:'));
271
- console.log(chalk.dim(` Components : ${summary.components}`));
272
- console.log(chalk.dim(` Agents : ${summary.agents}`));
273
- console.log(chalk.dim(` Target : ${summary.target}`));
274
- console.log(chalk.dim(` Mode : ${mode}`));
275
- console.log();
276
-
277
- const confirmed = await promptConfirmInstall(summary);
278
- if (!confirmed) {
279
- console.log(chalk.yellow('\n Installation cancelled.\n'));
280
- process.exit(0);
281
- }
282
-
283
- // ── Step 5: Install ──
284
- console.log();
285
- sectionHeading('Installing');
286
-
287
- if (components.includes('agents') && selectedAgents.length > 0) {
288
- await installAgents(selectedAgents, mode);
289
- }
290
-
291
- if (components.includes('agents-md')) {
292
- await installAgentsMd(mode);
293
- }
294
-
295
- if (components.includes('skill-bizar')) {
296
- await installSkill('bizar');
297
- }
234
+ const { existsSync } = await import('node:fs');
235
+ const { join, dirname } = await import('node:path');
236
+ const { fileURLToPath } = await import('node:url');
237
+ const { spawnSync } = await import('node:child_process');
298
238
 
299
- if (components.includes('skill-improve')) {
300
- await installSkill('self-improvement');
301
- }
302
-
303
- if (components.includes('skill-cpp-std')) {
304
- await installSkill('cpp-coding-standards');
305
- }
306
-
307
- if (components.includes('skill-cpp-test')) {
308
- await installSkill('cpp-testing');
309
- }
310
-
311
- if (components.includes('skill-esp-idf')) {
312
- await installSkill('embedded-esp-idf');
313
- }
314
-
315
- if (components.includes('opencode-json')) {
316
- await installOpencodeJson(mode);
317
- }
318
-
319
- // Always attempt to merge the 7 BizarHarness tool keys idempotently.
320
- // Safe to call even if opencode-json wasn't selected — skips silently.
321
- {
322
- const result = await mergeToolsIntoUserConfig();
323
- if (result.merged) {
324
- console.log(chalk.green(` ✓ ${result.added.length} BizarHarness tool key(s) merged into opencode.json`));
325
- }
326
- }
239
+ const __dirname = dirname(fileURLToPath(import.meta.url));
240
+ // cli/install.mjs → ../install.sh
241
+ const installSh = join(__dirname, '..', 'install.sh');
327
242
 
328
- if (components.includes('bizar')) {
329
- await installBizarFolder();
330
- }
331
-
332
- if (components.includes('plugin-bizar')) {
333
- const result = await installPluginBizar();
334
- if (result.errors.length > 0) {
335
- console.log(chalk.yellow(` ⚠ Plugin install: ${result.errors.length} error(s)`));
336
- }
243
+ if (!existsSync(installSh)) {
244
+ console.error(chalk.red(' ✗ install.sh not found at ' + installSh));
245
+ console.error(chalk.dim(' Run `git clone https://github.com/DrB0rk/BizarHarness` first, then `cd BizarHarness && ./install.sh`.'));
246
+ process.exit(1);
337
247
  }
338
248
 
339
- // Also try to install the plugin from the separate global npm package
340
- // `@polderlabs/bizar-plugin` (preferred path going forward).
341
- if (components.includes('plugin-bizar')) {
342
- await installPluginFromGlobal();
343
- }
249
+ console.log(chalk.bold.hex('#6366f1')('\n BizarHarness installer (delegating to install.sh)\n'));
344
250
 
345
- // ── Rules, hooks, commands (optional components) ──
346
- if (components.includes('rules')) {
347
- const n = await installRules();
348
- console.log(chalk.green(` ✓ ${n} rules installed`));
349
- }
350
- if (components.includes('hooks')) {
351
- const n = await installHooks();
352
- console.log(chalk.green(` ✓ ${n} hooks installed`));
353
- }
354
- if (components.includes('commands')) {
355
- const n = await installCommands();
356
- console.log(chalk.green(` ✓ ${n} commands installed`));
251
+ const useBash = process.platform !== 'win32' || process.env.WSL_DISTRO_NAME;
252
+ if (useBash) {
253
+ const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
254
+ process.exit(r.status ?? 1);
357
255
  }
358
256
 
359
- // ── RTK (always installed token optimization required) ──
360
- await installRtk();
361
-
362
- // ── Semble (always installed code search required) ──
363
- await installSemble();
364
-
365
- // ── Skills CLI (always installed — skill discovery required) ──
366
- await installSkillsCli();
367
-
368
- // ── Skill packs (via skills.sh ecosystem) ──
369
- for (const pack of skillPacks) {
370
- await installCuratedSkills([pack]);
371
- }
372
-
373
- // ── Step 6: API keys ──
374
- sectionHeading('API Keys');
375
- console.log(chalk.dim(' You can configure API keys now or later via /connect in opencode.'));
376
- const keys = await promptApiKeys();
377
-
378
- if (keys.opencodeZen || keys.minimax || keys.openai) {
379
- console.log(chalk.dim('\n Keys noted. Add them to your opencode.json or run /connect in opencode.\n'));
380
- }
381
-
382
- // ── Summary ──
383
- console.log();
384
- console.log(boxen(
385
- chalk.bold.hex('#6366f1')(' ⚡ BIZARHARNESS INSTALLED ⚡\n') +
386
- '\n' +
387
- chalk.dim(' Norse Pantheon active.\n') +
388
- '\n' +
389
- chalk.green(` ✓ ${selectedAgents.length} agents installed`) + '\n' +
390
- chalk.green(` ✓ ${summary.parts.length} components configured`) + '\n' +
391
- '\n' +
392
- chalk.green(' ✓ RTK ') + chalk.dim(`(${rtkInstalled ? 'already configured' : 'installed & configured'})`) + '\n' +
393
- chalk.green(' ✓ Semble ') + chalk.dim(`(${sembleInstalled ? 'ready' : 'installed'})`) + '\n' +
394
- chalk.green(' ✓ Skills CLI ') + chalk.dim(`(${skillsCliInstalled ? 'ready' : 'installed'})`) + '\n' +
395
- (skillPacks.length > 0 ? chalk.green(` ✓ Skill packs: ${skillPacks.join(', ')}`) + '\n' : '') + '\n' +
396
- chalk.hex('#a855f7')(' Next steps:') + '\n' +
397
- chalk.hex('#a855f7')(' 1. Restart opencode') + '\n' +
398
- chalk.hex('#a855f7')(' 2. Run /connect to add API keys') + '\n' +
399
- chalk.hex('#a855f7')(' 3. Run /models to verify agents'),
400
- {
401
- padding: 1,
402
- margin: 0,
403
- borderStyle: 'round',
404
- borderColor: '#6366f1',
405
- },
406
- ));
407
- console.log();
408
-
409
- // ── Provider auto-detect (v3.16.0) — best-effort, doesn't block install
410
- const { runProvidersDetect } = await import('./providers-detect.mjs');
411
- try {
412
- await runProvidersDetect(['--no-probe']);
413
- } catch {
414
- // best-effort
415
- }
416
-
417
- // ── Restart prompt ──
418
- const shouldRestart = await promptRestartOpenCode();
419
- if (shouldRestart) {
420
- console.log(chalk.dim(' Restarting opencode...'));
421
- try {
422
- const { execSync } = await import('node:child_process');
423
- execSync('opencode', { stdio: 'inherit' });
424
- } catch {
425
- console.log(chalk.yellow(' Could not restart automatically. Restart opencode manually.'));
426
- }
427
- }
428
-
429
- // ── Post-install: graphify ──
430
- await promptGraphifyInstall();
431
-
432
- // ── Post-install ──
433
- console.log(chalk.dim('\n Odin watches. The Pantheon awaits. ᛟ\n'));
257
+ // Windows without WSLbash isn't available. The bash script has a
258
+ // Windows fallback that uses npm-based install paths. Run the npm path
259
+ // directly: install the BizarHarness npm packages + copy plugin from
260
+ // global to ~/.config/opencode/plugins/bizar/. This is the same code
261
+ // path install.sh uses for `platform == win32`.
262
+ console.log(chalk.dim(' bash not available — running npm-based installer (Windows path)'));
263
+ console.log('');
264
+ await runPostInstall();
434
265
  }
435
266
 
436
267
  // ── Interactive prompts for optional packages ─────────────────────────────────
package/cli/update.mjs CHANGED
@@ -290,32 +290,38 @@ function rerunInstallScript() {
290
290
  return { ok: false, message: 'could not locate npm global root; skipping install-script rerun' };
291
291
  }
292
292
  const pkgRoot = join(globalRoot, ...PKG_MAIN.split('/'));
293
- const binPath = join(pkgRoot, 'cli', 'bin.mjs');
294
- if (existsSync(binPath)) {
295
- console.log(chalk.dim(`\n Re-running setup via ${binPath} --setup...`));
296
- const r = spawnSync(process.execPath, [binPath, '--setup'], { stdio: 'inherit' });
297
- if (r.status === 0) {
298
- return { ok: true, message: 'setup re-run' };
293
+ // v3.20.11: prefer the canonical `install.sh` script over `cli/bin.mjs
294
+ // --setup`. The bash script is the single source of truth (it handles
295
+ // system deps, chrome-headless-shell runtime libs, browser-harness,
296
+ // opencode.json merging, etc.); the --setup path is a legacy fallback
297
+ // for environments where bash isn't on PATH.
298
+ const installSh = join(pkgRoot, 'install.sh');
299
+ if (process.platform === 'win32') {
300
+ // On Windows without WSL, bash isn't available. The bash script has
301
+ // a Windows fallback that uses npm-based install paths. Fall through
302
+ // to the bin.mjs --setup path, which still does the agent copy +
303
+ // plugin-from-global install on Windows.
304
+ console.log(chalk.dim(' Windows: using bin.mjs --setup path (bash not available)'));
305
+ } else if (existsSync(installSh)) {
306
+ console.log(chalk.dim(`\n Re-running install script at ${installSh}...`));
307
+ const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
308
+ if (r.status !== 0) {
309
+ return { ok: false, message: 'install script rerun failed' };
299
310
  }
300
- return { ok: false, message: 'setup rerun failed' };
311
+ return { ok: true, message: 'install script re-run' };
312
+ } else {
313
+ console.log(chalk.dim(' install.sh not present — falling back to bin.mjs --setup'));
301
314
  }
302
- const installSh = join(pkgRoot, 'install.sh');
303
- if (!existsSync(installSh)) {
315
+ const binPath = join(pkgRoot, 'cli', 'bin.mjs');
316
+ if (!existsSync(binPath)) {
304
317
  return { ok: false, message: 'could not locate a compatible setup script to re-run' };
305
318
  }
306
- if (process.platform === 'win32') {
307
- // On Windows, the bash install path doesn't apply. The plugin is
308
- // already installed globally via the npm install
309
- // (see cli/install.mjs:installPluginFromGlobal).
310
- console.log(chalk.dim(' Skipping install.sh (Windows uses npm-based plugin install)'));
311
- return { ok: true, message: 'install.sh skipped on Windows' };
312
- }
313
- console.log(chalk.dim(`\n Re-running install script at ${installSh}...`));
314
- const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
315
- if (r.status !== 0) {
316
- return { ok: false, message: 'install script rerun failed' };
319
+ console.log(chalk.dim(`\n Re-running setup via ${binPath} --setup...`));
320
+ const r = spawnSync(process.execPath, [binPath, '--setup'], { stdio: 'inherit' });
321
+ if (r.status === 0) {
322
+ return { ok: true, message: 'setup re-run' };
317
323
  }
318
- return { ok: true, message: 'install script re-run' };
324
+ return { ok: false, message: 'setup rerun failed' };
319
325
  }
320
326
 
321
327
  // ---------------------------------------------------------------------------
package/install.sh ADDED
@@ -0,0 +1,562 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # install.sh — One-shot BizarHarness installer.
4
+ #
5
+ # v3.20.11 — Comprehensive auto-installer with full dependency
6
+ # resolution. Replaces the older "print 4 manual next steps" flow
7
+ # with: (1) install every system dep we can reasonably fetch
8
+ # (uv, python3.12, chrome-headless-shell, jq, Chrome runtime libs);
9
+ # (2) install browser-harness + register its skill; (3) start Chrome
10
+ # for browser-driven E2E; (4) install + configure all BizarHarness
11
+ # files (agents, commands, opencode.json, plugin); (5) print a
12
+ # single status banner that tells the operator what is ready and
13
+ # what (if anything) they still need to do.
14
+ #
15
+ # Idempotent — every step checks for the existing install and
16
+ # skips re-work. Re-running after a failed install picks up
17
+ # where it left off. Same script can run from a fresh `git clone`
18
+ # or via `bizar install` (which spawns this script under bash).
19
+ #
20
+ # What this script does NOT do (intentional):
21
+ # - Configure provider API keys (MINIMAX_API_KEY, etc.). The user
22
+ # does that via `/connect` in opencode after install. We deliberately
23
+ # do not collect secrets here — the install is idempotent and safe
24
+ # to re-run, and asking for keys during install breaks CI / scripted
25
+ # deployments.
26
+ # - Reload opencode. The opencode session picks up the new config on
27
+ # next start. We print a status line at the end; we do not kill or
28
+ # restart the opencode process from here (it would interrupt
29
+ # in-flight agent sessions).
30
+ # - Hide `.obsidian/`. The Obsidian vault is a first-class part of
31
+ # the project tree. We do not add it to any .gitignore / .npmignore
32
+ # / IDE-exclusion list.
33
+ #
34
+ # Idempotent: every step checks for the existing install and skips
35
+ # re-work. Re-running after a failed install picks up where it left off.
36
+ #
37
+ # Cross-platform: bash + curl work on Linux + macOS. On Windows run
38
+ # `npm install -g @polderlabs/bizar` which triggers the npm postinstall
39
+ # hook (cli/install.mjs → install.sh via git bash).
40
+ set -euo pipefail
41
+
42
+ BOLD='\033[1m'
43
+ GREEN='\033[0;32m'
44
+ YELLOW='\033[1;33m'
45
+ CYAN='\033[0;36m'
46
+ RED='\033[0;31m'
47
+ DIM='\033[2m'
48
+ NC='\033[0m'
49
+
50
+ REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
51
+ CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
52
+ SKILLS_DIR="$HOME/.opencode/skills"
53
+ BIZAR_STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/bizar"
54
+ BIZAR_STATE_FILE="$BIZAR_STATE_DIR/install-state.json"
55
+ PYTHON_BIN="${PYTHON_BIN:-python3.12}"
56
+ CHROME_PORT="${CHROME_PORT:-9222}"
57
+
58
+ # Counters for the final summary.
59
+ INSTALLED=()
60
+ SKIPPED=()
61
+ FAILED=()
62
+
63
+ note() {
64
+ echo -e " ${GREEN}✓${NC} $1"
65
+ INSTALLED+=("$1")
66
+ }
67
+ warn() {
68
+ echo -e " ${YELLOW}⚠${NC} $1"
69
+ SKIPPED+=("$1")
70
+ }
71
+ err() {
72
+ echo -e " ${RED}✗${NC} $1"
73
+ FAILED+=("$1")
74
+ }
75
+ section() {
76
+ echo ""
77
+ echo -e "${BOLD}${CYAN}── $1 ──${NC}"
78
+ }
79
+
80
+ # ── Pre-flight: which tools are missing? ───────────────────────────────
81
+ section "Pre-flight: checking system tools"
82
+
83
+ have_cmd() { command -v "$1" >/dev/null 2>&1; }
84
+ have_file() { [ -e "$1" ]; }
85
+
86
+ MISSING=()
87
+ have_cmd uv || MISSING+=("uv")
88
+ have_cmd "$PYTHON_BIN" || MISSING+=("$PYTHON_BIN")
89
+ have_cmd jq || MISSING+=("jq")
90
+ have_cmd npx || MISSING+=("npx")
91
+ have_cmd git || MISSING+=("git")
92
+
93
+ # Chrome detection: prefer chrome-headless-shell from puppeteer cache, then
94
+ # system chromium / chrome / google-chrome. We don't require Chrome — the
95
+ # installer works without it — but we report it so the operator knows.
96
+ CHROME_BIN=""
97
+ for cand in \
98
+ "$HOME/.cache/puppeteer/chrome-headless-shell"*/chrome-headless-shell/chrome-headless-shell \
99
+ /usr/bin/chromium \
100
+ /usr/bin/chrome \
101
+ /usr/bin/google-chrome \
102
+ "$(command -v chromium 2>/dev/null || true)" \
103
+ "$(command -v chrome 2>/dev/null || true)" \
104
+ "$(command -v google-chrome 2>/dev/null || true)"; do
105
+ [ -n "$cand" ] && [ -x "$cand" ] && CHROME_BIN="$cand" && break
106
+ done
107
+
108
+ for t in "${MISSING[@]}"; do warn "missing system tool: $t"; done
109
+ if [ -z "$CHROME_BIN" ]; then warn "no chrome / chromium found — browser-harness won't work until installed"; fi
110
+ [ ${#MISSING[@]} -eq 0 ] && [ -n "$CHROME_BIN" ] && note "all system tools present"
111
+
112
+ # ── Install missing system tools (best effort, never abort) ─────────────
113
+ section "Install missing system tools"
114
+
115
+ # uv (Python package manager — needed for browser-harness + Semble)
116
+ if ! have_cmd uv; then
117
+ echo -e " ${CYAN}→${NC} Installing uv (Python package manager)..."
118
+ if have_cmd curl; then
119
+ if curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1; then
120
+ # uv's installer drops the binary at ~/.local/bin/uv
121
+ export PATH="$HOME/.local/bin:$PATH"
122
+ if have_cmd uv; then
123
+ note "uv installed at $(command -v uv)"
124
+ else
125
+ warn "uv install ran but uv not on PATH — try 'export PATH=\$HOME/.local/bin:\$PATH'"
126
+ fi
127
+ else
128
+ warn "uv installer script failed — install manually: https://docs.astral.sh/uv/"
129
+ fi
130
+ else
131
+ warn "curl not found — install uv manually: https://docs.astral.sh/uv/"
132
+ fi
133
+ else
134
+ note "uv $(uv --version 2>/dev/null | head -1)"
135
+ fi
136
+
137
+ # python3.12 (uv will manage its own python via --python 3.12, but
138
+ # we also accept system python3.12 if present). Only install if uv
139
+ # is available — uv can fetch its own python.
140
+ if ! have_cmd "$PYTHON_BIN"; then
141
+ if have_cmd uv; then
142
+ echo -e " ${CYAN}→${NC} python3.12 not on PATH — uv will fetch on first use"
143
+ note "python3.12 will be auto-fetched by uv (browser-harness will trigger first fetch)"
144
+ else
145
+ warn "no $PYTHON_BIN and no uv — install manually: sudo apt install python3.12 / brew install python@3.12"
146
+ fi
147
+ else
148
+ note "$PYTHON_BIN $($PYTHON_BIN --version 2>&1 | head -1)"
149
+ fi
150
+
151
+ # jq (for safe opencode.json merging)
152
+ if ! have_cmd jq; then
153
+ echo -e " ${CYAN}→${NC} Trying to install jq..."
154
+ case "$(uname -s 2>/dev/null || echo unknown)" in
155
+ Linux)
156
+ if have_cmd apt-get; then sudo apt-get install -y jq 2>/dev/null && note "jq installed (apt)" || warn "apt install jq failed — install manually"
157
+ elif have_cmd dnf; then sudo dnf install -y jq 2>/dev/null && note "jq installed (dnf)" || warn "dnf install jq failed — install manually"
158
+ elif have_cmd pacman; then sudo pacman -S --noconfirm jq 2>/dev/null && note "jq installed (pacman)" || warn "pacman install jq failed — install manually"
159
+ elif have_cmd apk; then sudo apk add jq 2>/dev/null && note "jq installed (apk)" || warn "apk install jq failed — install manually"
160
+ else warn "no known package manager — install jq manually"
161
+ fi
162
+ ;;
163
+ Darwin)
164
+ if have_cmd brew; then brew install jq 2>/dev/null && note "jq installed (brew)" || warn "brew install jq failed — install manually"
165
+ else warn "no brew — install jq manually"
166
+ fi
167
+ ;;
168
+ *) warn "unknown OS — install jq manually";;
169
+ esac
170
+ else
171
+ note "jq $(jq --version)"
172
+ fi
173
+
174
+ # Chrome (best effort — install chrome-headless-shell from chrome-for-testing
175
+ # or the puppeteer cache). The dashboard/browser-harness will work without
176
+ # it (the user can install later), but we try.
177
+ if [ -z "$CHROME_BIN" ]; then
178
+ echo -e " ${CYAN}→${NC} Trying to install chrome-headless-shell..."
179
+ PUPPETEER_CACHE_DIR="$HOME/.cache/puppeteer"
180
+ mkdir -p "$PUPPETEER_CACHE_DIR"
181
+ # chrome-for-testing JSON API — find the latest stable chrome-headless-shell
182
+ # URL for linux64. Fall back to nothing if the request fails.
183
+ CHROME_CT_URL=$(curl -fsSL "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" 2>/dev/null \
184
+ | grep -o '"url": *"[^"]*chrome-headless-shell-linux64[^"]*"' \
185
+ | head -1 \
186
+ | sed 's/.*"url": *"\([^"]*\)".*/\1/')
187
+ if [ -n "$CHROME_CT_URL" ]; then
188
+ TMP_DIR=$(mktemp -d)
189
+ if curl -fsSL "$CHROME_CT_URL" -o "$TMP_DIR/chrome-headless-shell.zip" 2>/dev/null; then
190
+ TARGET_DIR="$PUPPETEER_CACHE_DIR/chrome-headless-shell"
191
+ mkdir -p "$TARGET_DIR"
192
+ unzip -q -o "$TMP_DIR/chrome-headless-shell.zip" -d "$TARGET_DIR"
193
+ CHROME_BIN=$(find "$TARGET_DIR" -name chrome-headless-shell -type f 2>/dev/null | head -1)
194
+ [ -n "$CHROME_BIN" ] && [ -x "$CHROME_BIN" ] && note "chrome-headless-shell installed at $CHROME_BIN" || warn "chrome-headless-shell install failed"
195
+ rm -rf "$TMP_DIR"
196
+ else
197
+ warn "download from chrome-for-testing failed — install Chrome manually"
198
+ fi
199
+ else
200
+ warn "could not resolve latest chrome-for-testing URL — install Chrome manually"
201
+ fi
202
+ fi
203
+
204
+ # Chrome runtime libs (v3.20.11). chrome-headless-shell needs ~10 shared
205
+ # libraries that Debian/Ubuntu don't ship by default. Without these,
206
+ # chrome starts but immediately errors with `libnspr4.so: cannot open
207
+ # shared object file`. We install them on apt-based systems; on others
208
+ # we warn but continue (Chrome won't work, but the rest of the install
209
+ # will).
210
+ if [ -n "$CHROME_BIN" ] || [ -z "$CHROME_BIN" ]; then
211
+ # Check if chrome-headless-shell actually runs (libs satisfied)
212
+ if [ -n "$CHROME_BIN" ] && ! "$CHROME_BIN" --version >/dev/null 2>&1; then
213
+ case "$(uname -s 2>/dev/null || echo unknown)" in
214
+ Linux)
215
+ if have_cmd apt-get; then
216
+ echo -e " ${CYAN}→${NC} chrome-headless-shell needs runtime libs — installing via apt..."
217
+ if sudo -n apt-get install -y --no-install-recommends \
218
+ libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
219
+ libxdamage1 libxkbcommon0 libasound2t64 libatspi2.0-0 \
220
+ 2>/dev/null; then
221
+ note "chrome runtime libs installed (libnss3, libnspr4, libatk*, libxkbcommon, libasound, libatspi)"
222
+ else
223
+ warn "apt install of chrome runtime libs failed (needs sudo). run manually:"
224
+ warn " sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \\"
225
+ warn " libxdamage1 libxkbcommon0 libasound2t64 libatspi2.0-0"
226
+ fi
227
+ else
228
+ warn "chrome-headless-shell runtime libs missing — install libnss3, libnspr4, libatk* for your distro"
229
+ fi
230
+ ;;
231
+ Darwin)
232
+ warn "chrome-headless-shell runtime libs missing — on macOS: brew install --cask chromium"
233
+ ;;
234
+ *) warn "chrome-headless-shell runtime libs missing — install via your package manager";;
235
+ esac
236
+ fi
237
+ fi
238
+
239
+ # ── npm packages (BizarHarness, optional @polderlabs/bizar-dash peer) ───
240
+ section "Install BizarHarness npm packages"
241
+
242
+ if have_cmd npm; then
243
+ for pkg in @polderlabs/bizar @polderlabs/bizar-dash; do
244
+ if npm ls -g "$pkg" --depth=0 >/dev/null 2>&1; then
245
+ cur=$(npm ls -g "$pkg" --depth=0 --json 2>/dev/null | grep -oE '"version":\s*"[^"]+"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
246
+ note "$pkg already installed (v$cur)"
247
+ else
248
+ echo -e " ${CYAN}→${NC} Installing $pkg..."
249
+ if npm install -g "$pkg" >/dev/null 2>&1; then
250
+ note "$pkg installed"
251
+ else
252
+ err "$pkg install failed (run manually: npm install -g $pkg)"
253
+ fi
254
+ fi
255
+ done
256
+ else
257
+ err "npm not found — install Node.js 20+ first: https://nodejs.org/"
258
+ fi
259
+
260
+ # ── browser-harness (Python tool via uv, requires Python 3.12) ─────────
261
+ section "Install browser-harness (Python via uv)"
262
+
263
+ if have_cmd browser-harness; then
264
+ note "browser-harness $(browser-harness --version 2>&1 | head -1) already installed"
265
+ else
266
+ if have_cmd uv; then
267
+ echo -e " ${CYAN}→${NC} uv tool install --python 3.12 --upgrade --force browser-harness"
268
+ if uv tool install --python 3.12 --upgrade --force browser-harness >/dev/null 2>&1; then
269
+ note "browser-harness installed"
270
+ else
271
+ err "browser-harness install failed (run manually: uv tool install --python 3.12 --upgrade --force browser-harness)"
272
+ fi
273
+ else
274
+ warn "uv not installed — skipping browser-harness (install uv first, then re-run this script)"
275
+ fi
276
+ fi
277
+
278
+ # Register the browser-harness skill so any agent that needs it picks it up.
279
+ if have_cmd browser-harness; then
280
+ BH_SKILL_DIR="$SKILLS_DIR/browser-harness"
281
+ mkdir -p "$BH_SKILL_DIR"
282
+ if browser-harness skill > "$BH_SKILL_DIR/SKILL.md" 2>/dev/null; then
283
+ note "browser-harness skill registered at $BH_SKILL_DIR/SKILL.md"
284
+ else
285
+ warn "browser-harness skill registration failed (re-run: browser-harness skill > $BH_SKILL_DIR/SKILL.md)"
286
+ fi
287
+ fi
288
+
289
+ # ── Chrome lifecycle (start browser-harness-up.sh) ────────────────────
290
+ section "Start Chrome for browser-harness"
291
+
292
+ if [ -x "$REPO_DIR/cli/browser-harness-up.sh" ]; then
293
+ # The script knows how to find chrome-headless-shell from the
294
+ # puppeteer cache (or any system chrome).
295
+ if BH_OUTPUT=$("$REPO_DIR/cli/browser-harness-up.sh" start 2>&1); then
296
+ # Trim verbose output; just keep the success lines.
297
+ while IFS= read -r line; do
298
+ case "$line" in
299
+ *"✓"*|*"already"*|*"stopped"*|*"restarted"*) note "$line" ;;
300
+ *) ;;
301
+ esac
302
+ done <<< "$BH_OUTPUT"
303
+ else
304
+ warn "browser-harness-up.sh start failed (run manually: $REPO_DIR/cli/browser-harness-up.sh start)"
305
+ fi
306
+ elif have_cmd "$REPO_DIR/../bin/bizar"; then
307
+ if "$REPO_DIR/../bin/bizar" browser-harness-up start 2>&1 | grep -E '✓|✗' | head -3; then
308
+ note "Chrome started via bizar browser-harness-up"
309
+ fi
310
+ else
311
+ warn "cli/browser-harness-up.sh not found at $REPO_DIR — skipping Chrome auto-start"
312
+ fi
313
+
314
+ # ── BizarHarness config files ─────────────────────────────────────────
315
+ section "Install BizarHarness config files"
316
+
317
+ mkdir -p "$CONFIG_DIR/agents"
318
+
319
+ # Agents — copy every .md except those already in place.
320
+ for f in "$REPO_DIR/config/agents/"*.md; do
321
+ [ -f "$f" ] || continue
322
+ name="$(basename "$f")"
323
+ if [ -f "$CONFIG_DIR/agents/$name" ] && cmp -s "$f" "$CONFIG_DIR/agents/$name"; then
324
+ : # identical, skip silently
325
+ else
326
+ cp "$f" "$CONFIG_DIR/agents/$name"
327
+ INSTALLED+=("agents/$name")
328
+ fi
329
+ done
330
+ note "agents synced ($(ls "$CONFIG_DIR/agents/"*.md 2>/dev/null | wc -l) files)"
331
+
332
+ # Shared baseline (used as a skill AND as the on-disk reference)
333
+ if [ -d "$REPO_DIR/config/agents/_shared" ]; then
334
+ mkdir -p "$CONFIG_DIR/agents/_shared"
335
+ cp -R "$REPO_DIR/config/agents/_shared/." "$CONFIG_DIR/agents/_shared/"
336
+ note "agents/_shared/ synced"
337
+ fi
338
+
339
+ # Slash commands
340
+ if [ -d "$REPO_DIR/config/commands/" ]; then
341
+ mkdir -p "$CONFIG_DIR/commands"
342
+ for f in "$REPO_DIR/config/commands/"*.md; do
343
+ [ -f "$f" ] || continue
344
+ name="$(basename "$f")"
345
+ cp "$f" "$CONFIG_DIR/commands/$name"
346
+ done
347
+ note "slash commands synced"
348
+ fi
349
+
350
+ # Hooks
351
+ if [ -d "$REPO_DIR/config/hooks/" ]; then
352
+ mkdir -p "$CONFIG_DIR/hooks"
353
+ for entry in "$REPO_DIR/config/hooks/"*; do
354
+ [ -e "$entry" ] || continue
355
+ name="$(basename "$entry")"
356
+ cp -R "$entry" "$CONFIG_DIR/hooks/$name"
357
+ done
358
+ note "hooks synced"
359
+ fi
360
+
361
+ # AGENTS.md
362
+ if [ -f "$REPO_DIR/config/AGENTS.md" ]; then
363
+ cp "$REPO_DIR/config/AGENTS.md" "$CONFIG_DIR/AGENTS.md"
364
+ note "AGENTS.md synced"
365
+ fi
366
+
367
+ # ── Bundled skills (bizar, self-improvement, agent-baseline, …) ───────
368
+ section "Install bundled skills"
369
+
370
+ mkdir -p "$SKILLS_DIR"
371
+ for skill in bizar self-improvement cpp-coding-standards cpp-testing embedded-esp-idf; do
372
+ if [ -d "$REPO_DIR/config/skills/$skill" ]; then
373
+ mkdir -p "$SKILLS_DIR/$skill"
374
+ cp -R "$REPO_DIR/config/skills/$skill/." "$SKILLS_DIR/$skill/"
375
+ note "skill: $skill"
376
+ else
377
+ warn "skill source missing: $skill (skipping)"
378
+ fi
379
+ done
380
+
381
+ # Shared agent baseline skill — referenced by every agent file.
382
+ if [ -f "$REPO_DIR/config/agents/_shared/AGENT_BASELINE.md" ]; then
383
+ mkdir -p "$SKILLS_DIR/agent-baseline"
384
+ cp "$REPO_DIR/config/agents/_shared/AGENT_BASELINE.md" "$SKILLS_DIR/agent-baseline/SKILL.md"
385
+ note "skill: agent-baseline (shared by all 14 agents)"
386
+ fi
387
+
388
+ # Make bundled scripts executable.
389
+ chmod +x "$SKILLS_DIR"/embedded-esp-idf/scripts/*.sh 2>/dev/null || true
390
+
391
+ # ── Domain skills via skills.sh (impeccable, ponytail, obsidian-skills) ─
392
+ section "Install domain skills from skills.sh"
393
+
394
+ if have_cmd npx; then
395
+ # Impeccable — UI anti-pattern detection (frontend quality)
396
+ if npx --yes impeccable skills install -y --scope=user --providers=opencode >/dev/null 2>&1; then
397
+ note "impeccable (UI anti-pattern detector)"
398
+ else
399
+ warn "impeccable install failed (run: npx impeccable skills install)"
400
+ fi
401
+
402
+ # Ponytail — minimal-code skill for AI agents
403
+ if npx --yes @dietrichgebert/ponytail install --scope=user >/dev/null 2>&1; then
404
+ note "ponytail (minimal-code skill)"
405
+ else
406
+ warn "ponytail install failed (run: npx @dietrichgebert/ponytail install)"
407
+ fi
408
+
409
+ # Obsidian skills — Obsidian Flavored Markdown, Bases, JSON Canvas, CLI
410
+ if npx --yes skills add https://github.com/kepano/obsidian-skills --all -y >/dev/null 2>&1; then
411
+ note "obsidian-skills (Obsidian Markdown/Bases/Canvas)"
412
+ else
413
+ warn "obsidian-skills install failed (run: npx skills add kepano/obsidian-skills)"
414
+ fi
415
+ else
416
+ warn "npx not found — skipping domain skill installation"
417
+ fi
418
+
419
+ # ── Bizar plugin (mirrored from npm package to ~/.config/opencode/plugins/) ─
420
+ section "Install Bizar opencode plugin"
421
+
422
+ PLUGIN_SRC="$REPO_DIR/plugins/bizar"
423
+ PLUGIN_DST="$CONFIG_DIR/plugins/bizar"
424
+ if [ -d "$PLUGIN_SRC" ]; then
425
+ mkdir -p "$PLUGIN_DST"
426
+ while IFS= read -r -d '' f; do
427
+ rel="${f#$PLUGIN_SRC/}"
428
+ mkdir -p "$(dirname "$PLUGIN_DST/$rel")"
429
+ cp "$f" "$PLUGIN_DST/$rel"
430
+ done < <(find "$PLUGIN_SRC" \
431
+ -not -path '*/node_modules/*' \
432
+ -not -path '*/dist/*' \
433
+ -not -name '*.log' \
434
+ -not -name '.DS_Store' \
435
+ -type f -print0)
436
+ note "plugins/bizar/ copied (excludes node_modules, dist, *.log)"
437
+ else
438
+ warn "Bizar plugin source not found at $PLUGIN_SRC — skipping"
439
+ fi
440
+
441
+ # ── Merge opencode.json (template + user's existing config) ────────────
442
+ section "Configure opencode.json"
443
+
444
+ # Prefer the .template copy; fall back to the legacy tracked file.
445
+ if [ -f "$REPO_DIR/config/opencode.json.template" ]; then
446
+ TEMPLATE="$REPO_DIR/config/opencode.json.template"
447
+ elif [ -f "$REPO_DIR/config/opencode.json" ]; then
448
+ TEMPLATE="$REPO_DIR/config/opencode.json"
449
+ else
450
+ warn "No opencode.json template found"
451
+ TEMPLATE=""
452
+ fi
453
+
454
+ if [ -n "$TEMPLATE" ]; then
455
+ if [ -f "$CONFIG_DIR/opencode.json" ]; then
456
+ cp "$CONFIG_DIR/opencode.json" "$CONFIG_DIR/opencode.json.bak"
457
+ fi
458
+
459
+ if have_cmd jq; then
460
+ MERGE_TMP="$CONFIG_DIR/opencode.json.merge.$$"
461
+ if [ -f "$CONFIG_DIR/opencode.json" ]; then
462
+ jq -s '.[0] * .[1]' "$TEMPLATE" "$CONFIG_DIR/opencode.json" > "$MERGE_TMP" 2>/dev/null \
463
+ && mv "$MERGE_TMP" "$CONFIG_DIR/opencode.json" \
464
+ || { rm -f "$MERGE_TMP"; cp "$TEMPLATE" "$CONFIG_DIR/opencode.json"; }
465
+ else
466
+ cp "$TEMPLATE" "$CONFIG_DIR/opencode.json"
467
+ fi
468
+
469
+ # Ensure the Bizar plugin entry is registered (idempotent).
470
+ PLUGIN_TMP="$CONFIG_DIR/opencode.json.tmp.$$"
471
+ jq '
472
+ if (.plugin // []) | map(.[0] == "./plugins/bizar/index.ts") | any then .
473
+ else .plugin = (.plugin // []) + [["./plugins/bizar/index.ts", {
474
+ "loopThresholdWarn": 5,
475
+ "loopThresholdEscalate": 8,
476
+ "loopThresholdBlock": 12,
477
+ "loopWindowSize": 10
478
+ }]]
479
+ end
480
+ ' "$CONFIG_DIR/opencode.json" > "$PLUGIN_TMP" \
481
+ && mv "$PLUGIN_TMP" "$CONFIG_DIR/opencode.json" \
482
+ || rm -f "$PLUGIN_TMP"
483
+ note "opencode.json (template merged with existing config; plugin entry ensured)"
484
+ else
485
+ cp "$TEMPLATE" "$CONFIG_DIR/opencode.json"
486
+ warn "jq not available — copied template directly. Re-install jq for safe merging."
487
+ fi
488
+ fi
489
+
490
+ # ── Write install-state.json (used by `bizar update` for migrations) ───
491
+ section "Write install-state"
492
+
493
+ mkdir -p "$BIZAR_STATE_DIR"
494
+ BH_VERSION=$(browser-harness --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' || echo "")
495
+ BIZAR_VERSION=$(npm ls -g @polderlabs/bizar --depth=0 --json 2>/dev/null | grep -oE '"@polderlabs/bizar":\s*\{[^}]*"version":\s*"[^"]+"' | grep -oE '"version":\s*"[^"]+"' | grep -oE '"[^"]+"' | tail -1 | tr -d '"' || echo "")
496
+ DASH_VERSION=$(npm ls -g @polderlabs/bizar-dash --depth=0 --json 2>/dev/null | grep -oE '"@polderlabs/bizar-dash":\s*\{[^}]*"version":\s*"[^"]+"' | grep -oE '"version":\s*"[^"]+"' | grep -oE '"[^"]+"' | tail -1 | tr -d '"' || echo "")
497
+ PY_VERSION=$("$PYTHON_BIN" --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' || echo "")
498
+ UV_VERSION=$(uv --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' || echo "")
499
+ JQ_VERSION=$(jq --version 2>/dev/null | sed 's/[^0-9.]//g' || echo "")
500
+
501
+ STATE_JSON=$(cat <<JSON
502
+ {
503
+ "installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S.%NZ | sed 's/\.[0-9]*//')",
504
+ "components": {
505
+ "bizar": "${BIZAR_VERSION:-unknown}",
506
+ "bizar-dash": "${DASH_VERSION:-unknown}",
507
+ "browser-harness": "${BH_VERSION:-unknown}",
508
+ "uv": "${UV_VERSION:-unknown}",
509
+ "python3.12": "${PY_VERSION:-unknown}",
510
+ "jq": "${JQ_VERSION:-unknown}"
511
+ }
512
+ }
513
+ JSON
514
+ )
515
+ echo "$STATE_JSON" > "$BIZAR_STATE_FILE"
516
+ note "install-state written to $BIZAR_STATE_FILE"
517
+
518
+ # ── Final status banner ──────────────────────────────────────────────
519
+ section "Install complete"
520
+
521
+ echo ""
522
+ echo -e "${BOLD}${CYAN}┌────────────────────────────────────────────────────────────┐${NC}"
523
+ echo -e "${BOLD}${CYAN}│${NC} ${BOLD}BizarHarness ready.${NC} │"
524
+ echo -e "${BOLD}${CYAN}│${NC} │"
525
+
526
+ if [ ${#INSTALLED[@]} -gt 0 ]; then
527
+ echo -e "${BOLD}${CYAN}│${NC} ${GREEN}✓ Installed:${NC} │"
528
+ for entry in "${INSTALLED[@]}"; do
529
+ short=$(echo "$entry" | head -c 56)
530
+ echo -e "${BOLD}${CYAN}│${NC} • $short$(printf '%*s' $((56 - ${#short})) '') │"
531
+ done
532
+ fi
533
+
534
+ if [ ${#SKIPPED[@]} -gt 0 ]; then
535
+ echo -e "${BOLD}${CYAN}│${NC} │"
536
+ echo -e "${BOLD}${CYAN}│${NC} ${YELLOW}⚠ Skipped:${NC} │"
537
+ for entry in "${SKIPPED[@]}"; do
538
+ short=$(echo "$entry" | head -c 56)
539
+ echo -e "${BOLD}${CYAN}│${NC} • $short$(printf '%*s' $((56 - ${#short})) '') │"
540
+ done
541
+ fi
542
+
543
+ if [ ${#FAILED[@]} -gt 0 ]; then
544
+ echo -e "${BOLD}${CYAN}│${NC} │"
545
+ echo -e "${BOLD}${CYAN}│${NC} ${RED}✗ Failed:${NC} │"
546
+ for entry in "${FAILED[@]}"; do
547
+ short=$(echo "$entry" | head -c 56)
548
+ echo -e "${BOLD}${CYAN}│${NC} • $short$(printf '%*s' $((56 - ${#short})) '') │"
549
+ done
550
+ fi
551
+
552
+ echo -e "${BOLD}${CYAN}│${NC} │"
553
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM}Next: open /connect in your next opencode session${NC} │"
554
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} to add API keys (opencode-zen is free; MiniMax${NC} │"
555
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} is paid). The installer never asks for keys —${NC} │"
556
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} do that part yourself so secrets stay on your${NC} │"
557
+ echo -e "${BOLD}${CYAN}│${NC} ${DIM} machine, not in any installer log.${NC} │"
558
+ echo -e "${BOLD}${CYAN}└────────────────────────────────────────────────────────────┘${NC}"
559
+ echo ""
560
+
561
+ [ ${#FAILED[@]} -gt 0 ] && exit 1
562
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "3.20.10",
3
+ "version": "3.20.12",
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": {
@@ -9,7 +9,9 @@
9
9
  "files": [
10
10
  "cli/",
11
11
  "config/",
12
- "templates/"
12
+ "templates/",
13
+ "install.sh",
14
+ "cli/browser-harness-up.sh"
13
15
  ],
14
16
  "workspaces": [
15
17
  "packages/*"