@polderlabs/bizar 4.5.1 → 4.7.0

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 (94) hide show
  1. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  2. package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
  3. package/bizar-dash/dist/assets/main-DGGq-iZI.js +361 -0
  4. package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-CWqPoGaT.js} +18 -19
  6. package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-i4Uv9eW8.js} +1 -2
  8. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-i4Uv9eW8.js.map} +1 -1
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  12. package/bizar-dash/src/server/headroom.mjs +37 -35
  13. package/bizar-dash/src/server/logger.mjs +71 -0
  14. package/bizar-dash/src/server/memory-lightrag.mjs +84 -37
  15. package/bizar-dash/src/server/metrics.mjs +193 -0
  16. package/bizar-dash/src/server/mods-loader.mjs +13 -5
  17. package/bizar-dash/src/server/providers-store.mjs +73 -1
  18. package/bizar-dash/src/server/routes/chat.mjs +41 -0
  19. package/bizar-dash/src/server/routes/lightrag.mjs +7 -2
  20. package/bizar-dash/src/server/routes/memory.mjs +13 -4
  21. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  22. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  23. package/bizar-dash/src/server/schedules-runner.mjs +4 -4
  24. package/bizar-dash/src/server/server.mjs +101 -37
  25. package/bizar-dash/src/server/watcher.mjs +2 -2
  26. package/bizar-dash/src/web/App.tsx +40 -6
  27. package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
  28. package/bizar-dash/src/web/components/Toast.tsx +1 -1
  29. package/bizar-dash/src/web/components/Topbar.tsx +1 -1
  30. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  31. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  32. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  33. package/bizar-dash/src/web/lib/api.ts +12 -11
  34. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  35. package/bizar-dash/src/web/locales/en.json +52 -0
  36. package/bizar-dash/src/web/main.tsx +5 -0
  37. package/bizar-dash/src/web/styles/chat.css +2 -0
  38. package/bizar-dash/src/web/styles/main.css +52 -23
  39. package/bizar-dash/src/web/styles/tasks.css +2 -0
  40. package/bizar-dash/src/web/views/Activity.tsx +24 -17
  41. package/bizar-dash/src/web/views/History.tsx +90 -75
  42. package/bizar-dash/src/web/views/Memory.tsx +4 -3
  43. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
  44. package/bizar-dash/src/web/views/Overview.tsx +15 -10
  45. package/bizar-dash/src/web/views/Settings.tsx +9 -2
  46. package/bizar-dash/src/web/views/Skills.tsx +3 -2
  47. package/bizar-dash/src/web/views/Tasks.tsx +4 -3
  48. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
  49. package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
  50. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  51. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  52. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  53. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  54. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  55. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  56. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  57. package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
  58. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  59. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  60. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  61. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  62. package/bizar-dash/tests/logger.test.mjs +207 -0
  63. package/bizar-dash/tests/metrics.test.mjs +183 -0
  64. package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
  65. package/bizar-dash/tests/setup.ts +7 -0
  66. package/bizar-dash/vitest.config.ts +13 -0
  67. package/cli/artifact-cli.mjs +605 -0
  68. package/cli/artifact-render.mjs +621 -0
  69. package/cli/artifact-server.mjs +847 -0
  70. package/cli/artifact.mjs +38 -2089
  71. package/cli/bg.mjs +5 -13
  72. package/cli/bin.mjs +170 -1348
  73. package/cli/commands/artifact.mjs +20 -0
  74. package/cli/commands/dash.mjs +160 -0
  75. package/cli/commands/headroom.mjs +204 -0
  76. package/cli/commands/install.mjs +169 -0
  77. package/cli/commands/memory.mjs +25 -0
  78. package/cli/commands/minimax.mjs +265 -0
  79. package/cli/commands/mod.mjs +185 -0
  80. package/cli/commands/service.mjs +65 -0
  81. package/cli/commands/usage.mjs +109 -0
  82. package/cli/commands/util.mjs +306 -0
  83. package/cli/doctor.mjs +6 -16
  84. package/cli/memory.mjs +65 -21
  85. package/cli/provision.mjs +10 -15
  86. package/cli/service-controller.mjs +1 -11
  87. package/cli/service.mjs +1 -11
  88. package/cli/utils.mjs +41 -1
  89. package/install.sh +1 -2
  90. package/package.json +8 -3
  91. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
  92. package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
  93. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
  94. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
package/cli/bin.mjs CHANGED
@@ -2,45 +2,68 @@
2
2
  /**
3
3
  * cli/bin.mjs
4
4
  *
5
- * v3.10.0 — `bizar` runtime CLI.
5
+ * v4.6 — `bizar` runtime CLI entrypoint.
6
6
  *
7
7
  * Architecture:
8
8
  * - `bizar` is the core runtime + installer + audit/init/export/update/artifact
9
9
  * + service + dash commands.
10
10
  * - The dashboard lives in `@polderlabs/bizar-dash` as a library.
11
- * Commands live under `bizar dash <subcommand>` (new canonical form).
12
- * `bizar dashboard` is a deprecated alias (still works, prints warning).
11
+ * - Subcommand implementations are in `cli/commands/*.mjs`.
13
12
  *
14
- * Subcommands:
15
- * install, audit, init, export, artifact, update, test-gate, service, dash
13
+ * Commands:
14
+ * install, audit, init, export, artifact, update, test-gate, service, dash,
15
+ * memory, headroom, minimax, usage, mod, doctor, repair, dev-link, dev-unlink,
16
+ * heads-up, bg, browser-harness-up, providers
16
17
  */
17
- import { existsSync } from 'node:fs';
18
- import { readFileSync, writeFileSync } from 'node:fs';
18
+ import chalk from 'chalk';
19
+ import { existsSync, readFileSync } from 'node:fs';
19
20
  import { homedir } from 'node:os';
20
- import { dirname, join } from 'node:path';
21
+ import { join } from 'node:path';
21
22
  import { fileURLToPath } from 'node:url';
22
-
23
- const HOME = homedir();
24
- const BIZAR_HOME = join(HOME, '.config', 'bizar');
25
- import chalk from 'chalk';
26
- import { runInstaller } from './install.mjs';
27
- import { runAudit } from './audit.mjs';
28
- import { runInit } from './init.mjs';
29
- import { runExport } from './export.mjs';
30
- import runArtifact from './artifact.mjs';
31
- import { runUpdate } from './update.mjs';
32
- import { runHeadsUp } from './heads-up.mjs';
33
23
  import { ensureSetup, checkSetupStatus } from './bootstrap.mjs';
34
24
 
25
+ // ── Exit codes ─────────────────────────────────────────────────────────────────
26
+ const EXIT_OK = 0;
27
+ const EXIT_ERROR = 1;
28
+ const EXIT_USAGE = 2;
29
+
30
+ // ── CLI version ───────────────────────────────────────────────────────────────
31
+
32
+ function readCliVersion() {
33
+ try {
34
+ const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url));
35
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
36
+ return pkg.version || 'unknown';
37
+ } catch {
38
+ return 'unknown';
39
+ }
40
+ }
41
+
42
+ // ── Argument parsing ──────────────────────────────────────────────────────────
43
+
35
44
  const args = process.argv.slice(2);
36
45
  const isHelpRequest = args.includes('--help') || args.includes('-h');
37
46
  const isVersionRequest = args.includes('--version') || args.includes('-v');
47
+ const wantJson = args.includes('--json');
48
+ const wantDebug = args.includes('--debug');
49
+
50
+ if (wantDebug) {
51
+ process.env.DEBUG = 'bizar:*';
52
+ process.env.BIZAR_DEBUG = '1';
53
+ }
38
54
 
39
- // ── Bootstrap ─────────────────────────────────────────────────────────────────
40
- // Every bin command checks setup status on first invocation.
41
- // Skip only when: --postinstall (manual trigger), --check (status only),
42
- // --help / --version (informational), BIZAR_SKIP_INSTALL=1 (disabled),
43
- // or already handled via npm script.
55
+ function dbg(...msg) {
56
+ if (wantDebug) console.error('[DEBUG]', ...msg);
57
+ }
58
+
59
+ // When invoked with ONLY a global flag (e.g. `bizar --help` or
60
+ // `bizar --version`), strip it so `cmd` is undefined and we fall
61
+ // through to showHelp() / readCliVersion(). When invoked as
62
+ // `bizar <cmd> --help`, the flag stays for the subcommand to handle.
63
+ const onlyGlobalFlag = args.length === 1 && (args[0] === '--help' || args[0] === '-h' || args[0] === '--version' || args[0] === '-v' || args[0] === '--json');
64
+ const dispatchArgs = onlyGlobalFlag ? [] : args;
65
+
66
+ // ── Bootstrap ──────────────────────────────────────────────────────────────────
44
67
  if (
45
68
  !args.includes('--postinstall') &&
46
69
  !args.includes('--check') &&
@@ -50,33 +73,19 @@ if (
50
73
  ) {
51
74
  await ensureSetup({ silent: true });
52
75
  }
53
- // ─────────────────────────────────────────────────────────────────────────────
54
76
 
55
- function readCliVersion() {
56
- try {
57
- const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url));
58
- const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
59
- return pkg.version || 'unknown';
60
- } catch {
61
- return 'unknown';
62
- }
63
- }
77
+ // ── Banner ─────────────────────────────────────────────────────────────────────
64
78
 
65
- function getBizarConfigDir() {
66
- if (process.platform === 'win32') {
67
- return process.env.APPDATA
68
- ? join(process.env.APPDATA, 'bizar')
69
- : join(process.env.HOME || process.cwd(), '.config', 'bizar');
70
- }
71
- return process.env.XDG_CONFIG_HOME
72
- ? join(process.env.XDG_CONFIG_HOME, 'bizar')
73
- : join(process.env.HOME || process.cwd(), '.config', 'bizar');
79
+ function showBanner() {
80
+ console.log(chalk.bold.cyan(' ᛭ Bizar — Norse Pantheon Agent System for opencode'));
81
+ console.log();
74
82
  }
75
83
 
84
+ // ── Help ──────────────────────────────────────────────────────────────────────
85
+
76
86
  function showHelp() {
87
+ showBanner();
77
88
  console.log(`
78
- Bizar — Norse Pantheon Agent System for opencode
79
-
80
89
  Usage:
81
90
  bizar <command> [options]
82
91
 
@@ -89,18 +98,20 @@ function showHelp() {
89
98
  test-gate Detect & run the project's test suite
90
99
  update Auto-update everything (opencode + bizar + dash + plugin)
91
100
  service Manage the background service daemon
92
- bg <subcommand> Manage background agents (list/view/kill/logs)
101
+ dash <subcommand> Manage the dashboard (start/stop/status/cleanup/tui)
93
102
  memory <subcommand> Manage project memory (Bizar Memory Service)
94
103
  headroom <subcommand> Manage Headroom context compression
95
- dash <subcommand> Manage the dashboard (start/stop/status/cleanup/tui)
96
- browser-harness-up Start Chromium for browser-harness (start/stop/status/restart)
97
- dev-link [src] Symlink the local plugin source into opencode's plugin dir
98
- dev-unlink Remove the dev symlink and restore the deployed copy
99
- doctor Check the BizarHarness install for health issues
100
- heads-up <subcommand> Manage pre-push / pre-release heads-ups (list/check/archive)
101
- minimax <subcommand> Manage MiniMax Token Plan integration (key + onboarding)
102
- usage Show compact usage analytics summary (24h rolling)
104
+ minimax <subcommand> Manage MiniMax Token Plan integration
103
105
  mod <subcommand> Manage mods (install/upgrade/list via the dashboard API)
106
+ usage Show compact usage analytics summary (24h rolling)
107
+ doctor Check the BizarHarness install for health issues
108
+ repair Fix common install issues
109
+ dev-link Symlink local plugin source into opencode's plugin dir
110
+ dev-unlink Remove the dev symlink and restore the deployed copy
111
+ heads-up <subcommand> Manage pre-push / pre-release heads-ups
112
+ bg <subcommand> Manage background agents (list/view/kill/logs)
113
+ browser-harness-up Start Chromium for browser-harness (start/stop/status)
114
+ providers detect Auto-detect provider API keys from env + opencode.json
104
115
 
105
116
  Examples:
106
117
  bizar install
@@ -108,1357 +119,168 @@ function showHelp() {
108
119
  bizar dash start
109
120
  bizar dash start --bg
110
121
  bizar dash stop
111
- bizar dash status
112
122
  bizar doctor
113
123
  bizar update --all --dry-run
114
124
 
115
125
  Run \`bizar <command> --help\` for per-command help.
116
126
 
117
127
  Install:
118
- npm install -g @polderlabs/bizar Install globally
119
- npm install -g @polderlabs/bizar-dash Optional dashboard package
120
- npm install -g @polderlabs/bizar-plugin Bizar opencode plugin
128
+ npm install -g @polderlabs/bizar
129
+ npm install -g @polderlabs/bizar-dash
130
+ npm install -g @polderlabs/bizar-plugin
121
131
  `);
122
132
  }
123
133
 
124
- function showAuditHelp() {
125
- console.log(`
126
- bizar audit — Run security audit on agent configuration
127
-
128
- Usage:
129
- bizar audit
130
- `);
131
- }
132
-
133
- function showInitHelp() {
134
- console.log(`
135
- bizar init — Initialize .bizar/ in current project
136
-
137
- Usage:
138
- bizar init
139
-
140
- Description:
141
- Detects the project stack, creates .bizar/PROJECT.md and
142
- .bizar/AGENTS_SELF_IMPROVEMENT.md and installs relevant skills.
143
- The per-project knowledge graph (in .bizar/graph/) is provided
144
- by the graphify mod — install it from the mod registry for
145
- that feature.
146
- `);
147
- }
148
-
149
- function showExportHelp() {
150
- console.log(`
151
- bizar export — Export agents/rules to another harness
152
-
153
- Usage:
154
- bizar export [claude|cursor|opencode]
155
-
156
- Description:
157
- Copies installed Bizar agents and rules into another harness format.
158
- `);
159
- }
160
-
161
- function showInstallHelp() {
162
- console.log(`
163
- bizar install — Run the unified BizarHarness installer
164
-
165
- Usage:
166
- bizar install Install (or refresh) every component
167
- bizar install --dry-run Print what would happen, change nothing
168
- bizar install --force Overwrite existing files
169
- bizar install --with-mods a,b,c Opt-in: install specific mods as part of the run
170
- bizar install --help Show this help
171
-
172
- Description:
173
- v4.4.7+ — unified installer. Same code path as 'bizar update'; the
174
- difference is just mode=install vs mode=update. Every step is
175
- idempotent — running this twice is safe.
176
-
177
- 1. Installs @polderlabs/bizar via npm (skipped if already current).
178
- 2. Shells to ./install.sh for platform-specific system deps (uv,
179
- python3.12, jq, gh on Linux; brew on macOS) + service registration
180
- (systemd / launchd / Task Scheduler).
181
- 3. Syncs agent files, slash commands, and bundled skills into
182
- ~/.config/opencode/.
183
- 4. Copies plugins/bizar/ from the npm install into
184
- ~/.config/opencode/plugins/bizar/ (preserves dev symlinks).
185
- 5. Patches ~/.config/opencode/opencode.json with the Bizar plugin
186
- entry (skipped if already present).
187
- 6. Runs 'bizar doctor' as a post-install health check.
188
-
189
- No API key collection, no interactive prompts.
190
- `);
191
- }
192
-
193
- function showUpdateHelp() {
194
- console.log(`
195
- bizar update — Update opencode + @polderlabs/bizar (which bundles the
196
- plugin and dashboard). Detects what's installed and only touches what's
197
- missing or out of date.
198
-
199
- Usage:
200
- bizar update Update EVERYTHING (default; auto-kills + restarts)
201
- bizar update --check Only print current vs. latest; do not update
202
- bizar update --channel=stable|beta Pick the npm dist-tag (default: stable)
203
- bizar update --no-restart Don't auto-restart the dashboard after update
204
- bizar update --dry-run Print what would happen, change nothing
205
- bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
206
- bizar update --yes Same as --force, but named for one-line scripts
207
- bizar update --with-mods a,b,c Opt-in: install specific mods as part of the run
208
- bizar update --help Show this help
209
-
210
- Components updated:
211
- opencode-ai the opencode CLI itself
212
- @polderlabs/bizar this CLI + dashboard + plugin (one package)
213
-
214
- Behavior (v4.4.7+):
215
- • Single unified provisioner. 'bizar install' and 'bizar update' are
216
- the same code path with different mode flags. Every step is
217
- idempotent — re-running is safe.
218
- • Detects running Bizar instances (background service daemon, web
219
- dashboard) by reading ~/.config/bizar/{service,dashboard}.pid and
220
- cleans up any stale or empty PID files.
221
- • Auto-kills running instances with a brief notice.
222
- • Sends SIGTERM, waits up to 5s, escalates to SIGKILL if needed.
223
- • Re-runs the install script so the deployed plugin source matches
224
- the just-upgraded npm version (avoids the version-skew trap).
225
- • If the dashboard was running and bizar was updated, spawns a
226
- fresh detached dashboard process with the new code (skipped with
227
- --no-restart).
228
- • Runs 'bizar doctor' after a successful update to catch config
229
- regressions before opencode tries to start.
230
- • With --check: prints the version matrix and release-notes excerpt
231
- between current and latest, exits non-zero if an update is available.
232
-
233
- Examples:
234
- bizar update Full auto-update (recommended)
235
- bizar update --check Show version matrix + notes, do nothing
236
- bizar update --channel=beta Upgrade to latest beta build
237
- bizar update --dry-run Preview what would change
238
- bizar update plugin --no-restart Plugin-only, leave dashboard alone
239
-
240
- Errors:
241
- Network failures (registry offline / DNS) and npm permission issues
242
- are surfaced with the raw npm output. The provisioner never silently
243
- swallows them — look for the ✗ marker in the step output.
244
- `);
245
- }
246
-
247
- function showTestGateHelp() {
248
- console.log(`
249
- bizar test-gate — Detect & run the project's test suite
250
- `);
251
- }
252
-
253
- function showDevLinkHelp() {
254
- console.log(`
255
- bizar dev-link / dev-unlink — Manage a symlink from the opencode plugin dir
256
- to a local source checkout, so edits propagate to opencode on next session.
257
-
258
- Usage:
259
- bizar dev-link [source-dir] Symlink source-dir (default: ./plugins/bizar)
260
- to ~/.config/opencode/plugins/bizar
261
- bizar dev-link --force Replace an existing deployed copy
262
- bizar dev-unlink Remove the dev symlink + restore from npm
263
- bizar dev-unlink --force Remove even if not a symlink (destructive)
134
+ // ── Command imports ────────────────────────────────────────────────────────────
264
135
 
265
- Description:
266
- By default, opencode loads the Bizar plugin from
267
- ~/.config/opencode/plugins/bizar, which is a real directory copied
268
- from the npm package. Edits to plugins/bizar/ in the BizarHarness
269
- repo don't propagate until you re-run the installer.
270
-
271
- \`bizar dev-link\` replaces that directory with a symlink pointing
272
- at your local checkout, so source edits are picked up immediately.
273
- \`bizar dev-unlink\` reverses the change by removing the symlink
274
- and re-installing the deployed copy from the npm package.
275
-
276
- While the dev link is in place, \`bizar update\` will skip the
277
- plugin-copy step (and print a warning) so it doesn't clobber the
278
- link. Run \`bizar dev-unlink\` first, or pass --force to overwrite.
279
-
280
- Examples:
281
- bizar dev-link
282
- bizar dev-link /home/me/projects/bizar/plugins/bizar
283
- bizar dev-link --force
284
- bizar dev-unlink
285
- `);
286
- }
287
-
288
- function showDoctorHelp() {
289
- console.log(`
290
- bizar doctor — Check the BizarHarness install for health issues
291
-
292
- Usage:
293
- bizar doctor
294
-
295
- Description:
296
- Runs a battery of health checks against the local install:
297
- • opencode CLI reachable
298
- • ~/.config/opencode/opencode.json parses as JSON
299
- • the Bizar plugin is registered
300
- • plugin path resolves
301
- • @polderlabs/bizar-plugin is installed globally
302
- • core agent files are installed (odin, quick, thor, tyr)
303
- • headroom / semble / skills on PATH (lenient — at least one)
304
- • dashboard reachable (skipped if no port file)
305
- • provider.minimax block + MiniMax model flags are sane
306
-
307
- Prints ✓/✗ for each check and a final summary. Exits non-zero
308
- if any check fails. Use \`bizar doctor\` after a manual config
309
- edit or to diagnose "why is opencode misbehaving?" questions.
310
-
311
- Related:
312
- bizar update Update + auto-run doctor on success
313
- `);
314
- }
315
-
316
- function showServiceHelp() {
317
- const bizarConfigDir = getBizarConfigDir();
318
- console.log(`
319
- bizar service — Manage the background service daemon
320
-
321
- Usage:
322
- bizar service start Start the service in background
323
- bizar service stop Stop the running service
324
- bizar service status Show whether the service is running
325
- bizar service logs Tail the service log
326
- bizar service follow Follow the service log until Ctrl-C
327
- bizar service install Register with systemd / launchd / scheduled task
328
- bizar service install --force Re-install even when the unit matches
329
- bizar service uninstall Remove the OS-level autostart
330
- bizar service uninstall --force Force-uninstall even when nothing is registered
331
-
332
- Description:
333
- The service watches per-project schedules (cron / interval / once)
334
- and runs them at the right time. It logs to
335
- ${bizarConfigDir}/service.log and writes its PID to
336
- ${bizarConfigDir}/service.pid.
337
-
338
- install registers the daemon under the OS init system — systemd user
339
- unit on Linux, launchd LaunchAgent on macOS, scheduled task
340
- ("BizarDashboardService", ONSTART, HIGHEST) on Windows. After
341
- install, a normal user does not need to run \`bizar service start\`
342
- for the dashboard background process — the OS does it at login.
343
- `);
344
- }
345
-
346
- function showModHelp() {
347
- console.log(`
348
- bizar mod — Manage mods (via the dashboard's HTTP API)
349
-
350
- Usage:
351
- bizar mod upgrade <id> [--backup] Upgrade an installed mod to the latest version
352
- bizar mod install <id> Install a mod from the registry
353
- bizar mod list List installed mods
354
- bizar mod registry Show registry URL + available mods
355
-
356
- Description:
357
- Subcommands call the running dashboard's HTTP API. If no dashboard is
358
- reachable, you'll be told to run \`bizar dash start\` first.
359
-
360
- \`bizar mod upgrade <id>\` will:
361
- 1. Snapshot the existing version of the mod
362
- 2. Optionally back up the folder (--backup)
363
- 3. Remove the existing copy (and its opencode-config instructions)
364
- 4. Install the latest version from the registry
365
- 5. Re-install the new mod's instruction files into opencode config
366
- 6. Print from-version → to-version
367
- `);
368
- }
369
-
370
- async function runModCommand(modArgs) {
371
- const sub = modArgs[0];
372
- const positional = modArgs.slice(1).filter((a) => !a.startsWith('-'));
373
- const flags = modArgs.slice(1).filter((a) => a.startsWith('-'));
374
-
375
- if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
376
- showModHelp();
377
- return;
378
- }
379
-
380
- // Read dashboard port from the port file the dashboard writes on start.
381
- const { readFileSync: rfs } = await import('node:fs');
382
- const { join: joinPath } = await import('node:path');
383
- const portFile = joinPath(getBizarConfigDir(), 'dashboard.port');
384
- let port = null;
136
+ async function importCommand(name) {
385
137
  try {
386
- port = parseInt(rfs(portFile, 'utf8').trim(), 10);
387
- if (!Number.isFinite(port) || port <= 0) port = null;
138
+ return await import(`./commands/${name}.mjs`);
388
139
  } catch {
389
- port = null;
390
- }
391
- if (!port) {
392
- console.error(chalk.red(' ✗ Dashboard is not running (no port file at ' + portFile + ').'));
393
- console.error(chalk.dim(' Start it first: `bizar dash start --bg`'));
394
- process.exit(1);
395
- }
396
- const baseUrl = `http://127.0.0.1:${port}`;
397
-
398
- async function postJson(path, body) {
399
- const res = await fetch(`${baseUrl}${path}`, {
400
- method: 'POST',
401
- headers: { 'Content-Type': 'application/json' },
402
- body: JSON.stringify(body || {}),
403
- });
404
- const text = await res.text();
405
- let json = null;
406
- try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
407
- if (!res.ok) {
408
- const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
409
- throw new Error(`${path} failed: ${msg}`);
410
- }
411
- return json;
412
- }
413
- async function getJson(path) {
414
- const res = await fetch(`${baseUrl}${path}`);
415
- const text = await res.text();
416
- let json = null;
417
- try { json = text ? JSON.parse(text) : null; } catch { /* keep raw */ }
418
- if (!res.ok) {
419
- const msg = json?.message || json?.error || text || `HTTP ${res.status}`;
420
- throw new Error(`${path} failed: ${msg}`);
421
- }
422
- return json;
423
- }
424
-
425
- if (sub === 'install') {
426
- const id = positional[0];
427
- if (!id) {
428
- console.error(chalk.red(' ✗ Missing mod id. Usage: bizar mod install <id>'));
429
- process.exit(1);
430
- }
431
- try {
432
- const m = await postJson('/api/mods', { id });
433
- console.log(chalk.green(` ✓ Installed "${m.id}" v${m.version}`));
434
- } catch (err) {
435
- console.error(chalk.red(` ✗ ${err.message}`));
436
- process.exit(1);
437
- }
438
- } else if (sub === 'upgrade') {
439
- const id = positional[0];
440
- if (!id) {
441
- console.error(chalk.red(' ✗ Missing mod id. Usage: bizar mod upgrade <id> [--backup]'));
442
- process.exit(1);
443
- }
444
- const backup = flags.includes('--backup') || flags.includes('-b');
445
- try {
446
- const r = await postJson(`/api/mods/${encodeURIComponent(id)}/upgrade`, { backup });
447
- const note = r.backupPath ? chalk.dim(` (backup: ${r.backupPath})`) : '';
448
- console.log(chalk.green(` ✓ Upgraded "${id}" v${r.from} → v${r.to}`) + note);
449
- } catch (err) {
450
- console.error(chalk.red(` ✗ ${err.message}`));
451
- process.exit(1);
452
- }
453
- } else if (sub === 'list') {
454
- try {
455
- const r = await getJson('/api/mods');
456
- const mods = r.mods || [];
457
- if (mods.length === 0) {
458
- console.log(chalk.dim(' (no mods installed)'));
459
- return;
460
- }
461
- for (const m of mods) {
462
- const state = m.enabled ? chalk.green('enabled ') : chalk.yellow('disabled');
463
- console.log(` ${m.id.padEnd(20)} v${m.version.padEnd(10)} ${state} ${m.name || ''}`);
464
- }
465
- } catch (err) {
466
- console.error(chalk.red(` ✗ ${err.message}`));
467
- process.exit(1);
468
- }
469
- } else if (sub === 'registry') {
470
- try {
471
- const r = await getJson('/api/mods/registry');
472
- console.log(chalk.dim(` Source: ${r.registry?.source || '(unknown)'}`));
473
- console.log(chalk.dim(` Updated: ${r.registry?.updatedAt || '(unknown)'}`));
474
- console.log('');
475
- const mods = r.mods || [];
476
- if (mods.length === 0) {
477
- console.log(chalk.dim(' (no mods in registry)'));
478
- return;
479
- }
480
- for (const m of mods) {
481
- const installed = m.installed ? chalk.green(`installed v${m.installedVersion || '?'}`) : chalk.dim('not installed');
482
- const upgrade = m.upgradeAvailable ? chalk.yellow(` ↑ v${m.upgradeAvailable} available`) : '';
483
- console.log(` ${m.id.padEnd(20)} v${(m.latest || '?').padEnd(10)} ${installed}${upgrade} ${m.name || ''}`);
484
- }
485
- } catch (err) {
486
- console.error(chalk.red(` ✗ ${err.message}`));
487
- process.exit(1);
488
- }
489
- } else {
490
- console.error(chalk.red(` ✗ Unknown mod subcommand: ${sub}`));
491
- showModHelp();
492
- process.exit(1);
140
+ return null;
493
141
  }
494
142
  }
495
143
 
496
- function showMinimaxHelp() {
497
- console.log(`
498
- bizar minimax — Manage the MiniMax Token Plan integration
499
-
500
- Usage:
501
- bizar minimax status Show whether the Subscription Key is configured
502
- and where it was resolved from (auth.json,
503
- opencode.json, env var, or none).
504
- bizar minimax remains Fetch the live 5-hour + weekly remaining
505
- quota per model. Shows reset times.
506
- bizar minimax test Smoke-test the key with a one-shot chat
507
- completion. Prints the usage block.
508
- bizar minimax config <key> Save a new Subscription Key to
509
- ~/.local/share/opencode/auth.json. The
510
- key never leaves this machine.
511
- bizar minimax clear Remove the Subscription Key from
512
- opencode's auth.json.
513
- bizar minimax reset-onboarding Re-trigger the first-run wizard. Use
514
- this if the key was changed outside the
515
- dashboard and you want to re-enter it.
516
-
517
- Flags:
518
- --base-url <url> Override the Token Plan host (default: https://www.minimax.io)
519
- --chat-url <url> Override the chat-completions host (default: https://api.minimax.io/v1)
520
- --yes Skip the confirmation prompt on 'clear' and 'reset-onboarding'
521
- `);
522
- }
523
-
524
- // Find the dashboard's port + password so the CLI can talk to /api/minimax/*.
525
- function readDashboardConn() {
526
- const portFile = join(BIZAR_HOME, 'dashboard.port');
527
- const authFile = join(BIZAR_HOME, 'dashboard-secret');
528
- let port = 4321;
529
- let secret = '';
530
- try {
531
- if (existsSync(portFile)) {
532
- const parsed = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
533
- if (Number.isFinite(parsed) && parsed > 0) port = parsed;
534
- }
535
- } catch { /* ignore */ }
536
- try {
537
- if (existsSync(authFile)) secret = readFileSync(authFile, 'utf8').trim();
538
- } catch { /* ignore */ }
539
- return { port, secret };
540
- }
541
-
542
- async function minimaxApi(path, opts = {}) {
543
- const { port, secret } = readDashboardConn();
544
- const url = `http://127.0.0.1:${port}${path}`;
545
- const headers = { 'content-type': 'application/json', accept: 'application/json' };
546
- if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
547
- const method = (opts.method || 'GET').toUpperCase();
548
- try {
549
- const resp = await fetch(url, { method, headers, body: opts.body ? JSON.stringify(opts.body) : undefined });
550
- const text = await resp.text();
551
- let data = null;
552
- try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
553
- if (!resp.ok) {
554
- return { ok: false, error: `http_${resp.status}`, message: data?.message || data?.error || resp.statusText, status: resp.status };
555
- }
556
- return { ok: true, status: resp.status, data };
557
- } catch (err) {
558
- return { ok: false, error: 'network_error', message: err && err.message ? err.message : String(err) };
559
- }
560
- }
561
-
562
- async function runMinimaxCommand(minimaxArgs) {
563
- const sub = minimaxArgs[0];
564
- const flags = minimaxArgs.slice(1).filter((a) => a.startsWith('-'));
565
- const positional = minimaxArgs.slice(1).filter((a) => !a.startsWith('-'));
566
- const yes = flags.includes('--yes') || flags.includes('-y');
567
-
568
- if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
569
- showMinimaxHelp();
570
- return;
571
- }
572
-
573
- if (sub === 'status') {
574
- const r = await minimaxApi('/api/minimax/status');
575
- if (!r.ok) {
576
- console.error(chalk.red(` ✗ ${r.message || r.error}`));
577
- process.exit(1);
578
- }
579
- const s = r.data;
580
- console.log('');
581
- console.log(chalk.bold(' MiniMax Token Plan status'));
582
- console.log('');
583
- console.log(` ${chalk.dim('configured:')} ${s.configured ? chalk.green('yes') : chalk.red('no')}`);
584
- console.log(` ${chalk.dim('key source:')} ${chalk.cyan(s.source)}`);
585
- if (s.apiKeyHint) console.log(` ${chalk.dim('key hint:')} ${s.apiKeyHint}`);
586
- console.log(` ${chalk.dim('group id:')} ${s.groupId}`);
587
- console.log(` ${chalk.dim('token host:')} ${s.tokenBaseUrl}`);
588
- console.log(` ${chalk.dim('chat host:')} ${s.chatBaseUrl}`);
589
- console.log(` ${chalk.dim('key format:')} ${s.keyPatternValid === true ? chalk.green('ok') : s.keyPatternValid === false ? chalk.red('unexpected prefix') : chalk.dim('n/a')}`);
590
- console.log('');
591
- if (s.cache) {
592
- console.log(` ${chalk.dim('cached:')} ${new Date(s.cache.fetchedAt).toLocaleString()} (${s.cache.modelCount} models)`);
593
- } else {
594
- console.log(` ${chalk.dim('cached:')} none yet — run \`bizar minimax remains\` to populate`);
595
- }
596
- return;
597
- }
598
-
599
- if (sub === 'remains') {
600
- const r = await minimaxApi('/api/minimax/remains');
601
- if (!r.ok) {
602
- console.error(chalk.red(` ✗ ${r.message || r.error}`));
603
- process.exit(1);
604
- }
605
- if (!r.data?.ok) {
606
- console.error(chalk.red(` ✗ ${r.data?.message || r.data?.error || 'unknown'}`));
607
- process.exit(1);
608
- }
609
- const data = r.data;
610
- console.log('');
611
- console.log(chalk.bold(` MiniMax Token Plan quota (${new Date(data.fetchedAt).toLocaleString()})`));
612
- if (data.apiKeyHint) console.log(chalk.dim(` Key: ${data.apiKeyHint} · source: ${data.keySource} · group: ${data.groupId}`));
613
- console.log('');
614
- for (const m of data.models || []) {
615
- const five = m.current_interval_remaining_percent;
616
- const week = m.current_weekly_remaining_percent;
617
- const fiveBar = '█'.repeat(Math.round(five / 5)) + '░'.repeat(20 - Math.round(five / 5));
618
- const weekBar = '█'.repeat(Math.round(week / 5)) + '░'.repeat(20 - Math.round(week / 5));
619
- const fiveColor = five >= 75 ? chalk.green : five >= 25 ? chalk.yellow : chalk.red;
620
- const weekColor = week >= 75 ? chalk.green : week >= 25 ? chalk.yellow : chalk.red;
621
- console.log(` ${chalk.bold(m.model_name)}`);
622
- console.log(` ${chalk.dim('5h:')} ${fiveColor(five + '%'.padStart(4))} ${fiveBar} resets in ${m.intervalResetInHuman}`);
623
- console.log(` ${chalk.dim('week:')} ${weekColor(week + '%'.padStart(4))} ${weekBar} resets in ${m.weeklyResetInHuman}`);
624
- console.log('');
625
- }
626
- return;
627
- }
628
-
629
- if (sub === 'test') {
630
- const prompt = positional[0] || 'Reply with the single word: pong';
631
- const r = await minimaxApi('/api/minimax/test', {
632
- method: 'POST',
633
- body: { prompt, model: 'MiniMax-M3', maxTokens: 32 },
634
- });
635
- if (!r.ok) {
636
- console.error(chalk.red(` ✗ ${r.message || r.error}`));
637
- process.exit(1);
638
- }
639
- const data = r.data;
640
- if (!data?.ok) {
641
- console.error(chalk.red(` ✗ ${data?.message || data?.error || 'unknown'}`));
642
- process.exit(1);
643
- }
644
- console.log('');
645
- console.log(chalk.green(' ✓ Key works'));
646
- console.log(` ${chalk.dim('model:')} ${data.model}`);
647
- console.log(` ${chalk.dim('finish:')} ${data.finishReason}`);
648
- if (data.content) console.log(` ${chalk.dim('content:')} ${JSON.stringify(data.content.slice(0, 80))}${data.content.length > 80 ? '…' : ''}`);
649
- if (data.usage) {
650
- console.log(` ${chalk.dim('usage:')} total=${data.usage.total_tokens} prompt=${data.usage.prompt_tokens} completion=${data.usage.completion_tokens}`);
651
- }
652
- return;
653
- }
144
+ // ── Main ───────────────────────────────────────────────────────────────────────
654
145
 
655
- if (sub === 'config' || sub === 'set') {
656
- const key = positional[0];
657
- if (!key) {
658
- console.error(chalk.red(' ✗ Missing key. Usage: bizar minimax config <sk-cp-…>'));
659
- process.exit(1);
660
- }
661
- if (!/^sk-(cp|ant|or)-[A-Za-z0-9_-]{20,}$/.test(key)) {
662
- console.error(chalk.red(' ✗ Key does not look like a MiniMax key (expected sk-cp-…, sk-ant-…, or sk-or-… prefix)'));
663
- process.exit(1);
664
- }
665
- console.log(chalk.dim(' Saving to opencode auth.json…'));
666
- const r = await minimaxApi('/api/minimax/onboarding/save-key', {
667
- method: 'POST',
668
- body: { key, groupId: 'default' },
669
- });
670
- if (!r.ok) {
671
- console.error(chalk.red(` ✗ ${r.message || r.error}`));
672
- process.exit(1);
673
- }
674
- console.log(chalk.green(` ✓ Saved to ${r.data?.path}`));
675
- console.log(` ${chalk.dim('key hint:')} ${r.data?.apiKeyHint}`);
146
+ async function main() {
147
+ if (args.includes('--check')) {
148
+ const status = checkSetupStatus();
149
+ if (wantJson) process.stdout.write(JSON.stringify(status, null, 2) + '\n');
150
+ else console.log(JSON.stringify(status, null, 2));
151
+ process.exit(status.needed ? EXIT_ERROR : EXIT_OK);
676
152
  return;
677
153
  }
678
154
 
679
- if (sub === 'clear' || sub === 'remove') {
680
- if (!yes) {
681
- console.log(chalk.yellow(` ⚠ This will remove the MiniMax Subscription Key from opencode's auth.json.`));
682
- console.log(chalk.dim(' Continue? [y/N]'));
683
- // simple readline confirmation
684
- const buf = [];
685
- process.stdin.setEncoding('utf8');
686
- process.stdin.on('data', (c) => { buf.push(c); if (c.includes('\n')) process.stdin.pause(); });
687
- await new Promise((resolve) => process.stdin.once('close', resolve));
688
- const answer = buf.join('').trim().toLowerCase();
689
- if (answer !== 'y' && answer !== 'yes') {
690
- console.log(chalk.dim(' Cancelled.'));
691
- return;
692
- }
693
- }
694
- // We don't have a "clear key" route; instead write an empty auth.json
695
- // entry. We piggyback on the onboarding save-key by saving an empty
696
- // marker isn't allowed — use the providers-store side instead. For
697
- // now, recommend using the dashboard's "clear" button. But the
698
- // simplest CLI path is to write auth.json directly.
699
- const authFile = join(HOME, '.local', 'share', 'opencode', 'auth.json');
700
- let auth = {};
701
- try {
702
- if (existsSync(authFile)) auth = JSON.parse(readFileSync(authFile, 'utf8'));
703
- } catch { /* ignore */ }
704
- if (auth.minimax) {
705
- delete auth.minimax;
706
- writeFileSync(authFile, JSON.stringify(auth, null, 2) + '\n', 'utf8');
707
- console.log(chalk.green(' ✓ MiniMax key removed from auth.json'));
708
- } else {
709
- console.log(chalk.dim(' No MiniMax key was configured.'));
710
- }
155
+ if (args.includes('--setup') || args.includes('--postinstall')) {
156
+ await ensureSetup({ silent: false });
157
+ process.exit(EXIT_OK);
711
158
  return;
712
159
  }
713
160
 
714
- if (sub === 'reset-onboarding' || sub === 'reset') {
715
- if (!yes) {
716
- console.log(chalk.yellow(' ⚠ This will re-trigger the first-run MiniMax onboarding wizard.'));
717
- console.log(chalk.dim(' Continue? [y/N]'));
718
- const buf = [];
719
- process.stdin.setEncoding('utf8');
720
- process.stdin.on('data', (c) => { buf.push(c); if (c.includes('\n')) process.stdin.pause(); });
721
- await new Promise((resolve) => process.stdin.once('close', resolve));
722
- const answer = buf.join('').trim().toLowerCase();
723
- if (answer !== 'y' && answer !== 'yes') {
724
- console.log(chalk.dim(' Cancelled.'));
725
- return;
726
- }
727
- }
728
- // Reset via the dashboard API.
729
- const r = await minimaxApi('/api/minimax/onboarding', {
730
- method: 'POST',
731
- body: { dismissedAt: null },
732
- });
733
- if (!r.ok) {
734
- console.error(chalk.red(` ✗ ${r.message || r.error}`));
735
- process.exit(1);
736
- }
737
- console.log(chalk.green(' ✓ Onboarding wizard will show on next dashboard load'));
161
+ if (isVersionRequest) {
162
+ console.log(readCliVersion());
738
163
  return;
739
164
  }
740
165
 
741
- console.error(chalk.red(` ✗ Unknown minimax subcommand: ${sub}`));
742
- showMinimaxHelp();
743
- process.exit(1);
744
- }
745
-
746
- // ── Usage subcommand ───────────────────────────────────────────────────────
747
-
748
- function showUsageHelp() {
749
- console.log(`
750
- bizar usage — Show compact usage analytics summary
166
+ const [cmd, ...cmdArgs] = dispatchArgs;
751
167
 
752
- Usage:
753
- bizar usage [24h|7d|30d] Show summary for the given range (default: 24h)
754
-
755
- Examples:
756
- bizar usage
757
- bizar usage 7d
758
- bizar usage 30d
759
- `);
760
- }
761
-
762
- async function runUsageCommand(args) {
763
- const range = (args[0] && ['24h', '7d', '30d'].includes(args[0])) ? args[0] : '24h';
764
- const { port, secret } = readDashboardConn();
765
- const url = `http://127.0.0.1:${port}/api/usage?range=${range}`;
766
- const headers = { accept: 'application/json' };
767
- if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
768
- try {
769
- const resp = await fetch(url, { method: 'GET', headers });
770
- const text = await resp.text();
771
- let data = null;
772
- try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
773
- if (!resp.ok || !data) {
774
- console.error(chalk.red(` ✗ Failed to load usage data: ${data?.message ?? resp.statusText}`));
775
- process.exit(1);
776
- }
777
- const t = data.totals;
778
- console.log('');
779
- console.log(chalk.bold(` Usage summary — ${range} (from JSONL store)`));
780
- console.log('');
781
- console.log(` ${chalk.dim('Requests:')} ${t.requests.toLocaleString()} (${t.errors} errors)`);
782
- console.log(` ${chalk.dim('Tokens:')} ${t.totalTokens.toLocaleString()} total (${t.promptTokens.toLocaleString()} prompt · ${t.completionTokens.toLocaleString()} completion)`);
783
- console.log(` ${chalk.dim('Cached:')} ${t.cachedTokens.toLocaleString()} tokens`);
784
- console.log(` ${chalk.dim('Reasoning:')} ${t.reasoningTokens.toLocaleString()} tokens`);
785
- console.log(` ${chalk.dim('Avg latency:')} ${t.avgLatencyMs}ms (p95: ${t.p95LatencyMs}ms)`);
786
- if (t.costEstimate > 0) {
787
- console.log(` ${chalk.dim('Est. cost:')} $${t.costEstimate.toFixed(4)} USD`);
168
+ if (!cmd) {
169
+ if (isVersionRequest) {
170
+ console.log(readCliVersion());
171
+ return;
788
172
  }
789
- console.log('');
790
- if (data.daily && data.daily.length > 0) {
791
- console.log(chalk.dim(` ${chalk.bold('Daily breakdown')}`));
792
- for (const day of data.daily.slice(-7)) {
793
- const barLen = Math.round((day.totalTokens / Math.max(...data.daily.map(d => d.totalTokens))) * 20);
794
- const bar = '█'.repeat(barLen) + '░'.repeat(20 - barLen);
795
- console.log(` ${day.date} ${bar} ${day.totalTokens.toLocaleString()} tok ${day.requests} req`);
796
- }
797
- }
798
- if (data.perModel && data.perModel.length > 0) {
799
- console.log('');
800
- console.log(chalk.dim(` ${chalk.bold('Per model')}`));
801
- for (const m of data.perModel.slice(0, 8)) {
802
- console.log(` ${m.modelId.padEnd(24)} ${String(m.requests).padStart(6)} req ${String(m.totalTokens).padStart(8)} tok`);
803
- }
804
- }
805
- console.log('');
806
- } catch (err) {
807
- console.error(chalk.red(` ✗ Network error: ${err && err.message ? err.message : String(err)}`));
808
- console.error(chalk.dim(' Is the dashboard running? Run `bizar dash start` first.'));
809
- process.exit(1);
810
- }
811
- }
812
-
813
- function showHeadroomHelp() {
814
- console.log(`
815
- bizar headroom — Manage Headroom context compression
816
-
817
- Usage:
818
- bizar headroom status Show live status (installed, proxy, wrapped)
819
- bizar headroom stats Show compression stats for the last 24h
820
- bizar headroom install Install headroom via pip or npm
821
- bizar headroom wrap Wrap opencode to route through the proxy
822
- bizar headroom unwrap Unwrap opencode
823
- bizar headroom start Start the proxy server
824
- bizar headroom stop Stop the proxy server
825
- bizar headroom doctor Run headroom doctor health check
826
-
827
- Examples:
828
- bizar headroom status
829
- bizar headroom stats
830
- bizar headroom install
831
- bizar headroom wrap
832
- `);
833
- }
834
-
835
- async function runHeadroomCommand(headroomArgs) {
836
- const sub = headroomArgs[0];
837
- const flags = headroomArgs.slice(1).filter((a) => a.startsWith('-'));
838
- const positional = headroomArgs.slice(1).filter((a) => !a.startsWith('-'));
839
-
840
- if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
841
- showHeadroomHelp();
173
+ showHelp();
174
+ process.exit(EXIT_OK);
842
175
  return;
843
176
  }
844
177
 
845
- // Delegate to dashboard API
846
- const { port, secret } = readDashboardConn();
847
- const baseUrl = `http://127.0.0.1:${port}`;
848
-
849
- async function apiGet(path) {
850
- const url = `${baseUrl}${path}`;
851
- const headers = { accept: 'application/json' };
852
- if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
853
- const res = await fetch(url, { method: 'GET', headers });
854
- const text = await res.text();
855
- let data = null;
856
- try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
857
- if (!res.ok) throw new Error(`${path}: ${data?.message || data?.error || res.statusText}`);
858
- return data;
859
- }
860
-
861
- async function apiPost(path, body = {}) {
862
- const url = `${baseUrl}${path}`;
863
- const headers = { 'content-type': 'application/json', accept: 'application/json' };
864
- if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
865
- const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
866
- const text = await res.text();
867
- let data = null;
868
- try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
869
- if (!res.ok) throw new Error(`${path}: ${data?.message || data?.error || res.statusText}`);
870
- return data;
871
- }
872
-
873
- if (sub === 'status') {
874
- const s = await apiGet('/api/headroom/status');
875
- console.log('');
876
- console.log(chalk.bold(' Headroom status'));
877
- console.log('');
878
- console.log(` ${chalk.dim('installed:')} ${s.installed ? chalk.green('yes') : chalk.red('no')} ${s.version || ''}`);
879
- console.log(` ${chalk.dim('proxy:')} ${s.proxyRunning ? chalk.green('running') : chalk.yellow('stopped')} ${s.proxyPort ? `@ ${s.proxyPort}` : ''} ${s.proxyPid ? `(PID ${s.proxyPid})` : ''}`);
880
- console.log(` ${chalk.dim('wrapped:')} ${s.wrapped ? chalk.green('yes') : chalk.yellow('no')}`);
881
- console.log(` ${chalk.dim('healthy:')} ${s.healthy === 'ok' ? chalk.green('ok') : s.healthy === 'warn' ? chalk.yellow('warn') : chalk.red('fail')}`);
882
- if (s.messages && s.messages.length > 0) {
883
- console.log('');
884
- for (const m of s.messages) {
885
- console.log(` ${m}`);
886
- }
178
+ if (isHelpRequest && !cmd.startsWith('-')) {
179
+ // Pass --help to the command
180
+ const mod = await importCommand(cmd);
181
+ if (mod && typeof mod.run === 'function') {
182
+ await mod.run(cmd, cmdArgs, true);
183
+ return;
887
184
  }
888
- console.log('');
889
- return;
890
185
  }
891
186
 
892
- if (sub === 'stats') {
893
- const hours = parseInt(positional[0], 10) || 24;
894
- const st = await apiGet(`/api/headroom/stats?hours=${hours}`);
895
- console.log('');
896
- console.log(chalk.bold(` Headroom stats (${hours}h)`));
897
- console.log('');
898
- if (st.error) {
899
- console.log(chalk.yellow(` ${st.error}`));
900
- } else {
901
- console.log(` ${chalk.dim('tokens saved:')} ${(st.tokensSaved || 0).toLocaleString()}`);
902
- console.log(` ${chalk.dim('compression:')} ${st.compressionRatio ? `${Math.round(st.compressionRatio * 100)}%` : '—'}`);
903
- console.log(` ${chalk.dim('cache hits:')} ${st.cacheHits ?? '—'}`);
904
- console.log(` ${chalk.dim('transforms:')} ${st.transforms ?? '—'}`);
187
+ // Dispatch to command modules
188
+ switch (cmd) {
189
+ case 'install':
190
+ case 'update': {
191
+ const mod = await importCommand('install');
192
+ await mod.run(cmd, cmdArgs, isHelpRequest);
193
+ break;
905
194
  }
906
- console.log('');
907
- return;
908
- }
909
195
 
910
- if (sub === 'install') {
911
- const r = await apiPost('/api/headroom/install', { force: true });
912
- if (r.installed) {
913
- console.log(chalk.green(` ✓ Headroom installed via ${r.method}${r.version ? ` (${r.version})` : ''}`));
914
- } else {
915
- console.log(chalk.red(' ✗ Install failed. Try: pip install "headroom-ai[all]"'));
196
+ case 'service': {
197
+ const mod = await importCommand('service');
198
+ await mod.run(cmd, cmdArgs, isHelpRequest);
199
+ break;
916
200
  }
917
- return;
918
- }
919
201
 
920
- if (sub === 'wrap') {
921
- const port = parseInt(positional[0], 10) || 8787;
922
- const r = await apiPost('/api/headroom/wrap', { port });
923
- if (r.ok) {
924
- console.log(chalk.green(` ✓ opencode wrapped with Headroom on port ${port}`));
925
- } else {
926
- console.log(chalk.red(` ✗ Wrap failed: ${r.log || 'unknown error'}`));
202
+ case 'dash':
203
+ case 'dashboard': {
204
+ const mod = await importCommand('dash');
205
+ await mod.run(cmd, cmdArgs, isHelpRequest);
206
+ break;
927
207
  }
928
- return;
929
- }
930
208
 
931
- if (sub === 'unwrap') {
932
- const r = await apiPost('/api/headroom/unwrap');
933
- if (r.ok) {
934
- console.log(chalk.green(' ✓ opencode unwrapped from Headroom'));
935
- } else {
936
- console.log(chalk.red(' ✗ Unwrap failed'));
209
+ case 'minimax': {
210
+ const mod = await importCommand('minimax');
211
+ await mod.run(cmd, cmdArgs, isHelpRequest);
212
+ break;
937
213
  }
938
- return;
939
- }
940
214
 
941
- if (sub === 'start') {
942
- const port = parseInt(positional[0], 10) || 8787;
943
- const r = await apiPost('/api/headroom/proxy/start', { port, host: '127.0.0.1' });
944
- if (r.ok) {
945
- console.log(chalk.green(` ✓ Proxy started on 127.0.0.1:${port} (PID ${r.pid})`));
946
- } else {
947
- console.log(chalk.red(' ✗ Proxy start failed'));
215
+ case 'headroom': {
216
+ const mod = await importCommand('headroom');
217
+ await mod.run(cmd, cmdArgs, isHelpRequest);
218
+ break;
948
219
  }
949
- return;
950
- }
951
220
 
952
- if (sub === 'stop') {
953
- const r = await apiPost('/api/headroom/proxy/stop');
954
- if (r.ok) {
955
- console.log(chalk.green(' ✓ Proxy stopped'));
956
- } else {
957
- console.log(chalk.red(' ✗ Proxy stop failed'));
221
+ case 'mod': {
222
+ const mod = await importCommand('mod');
223
+ await mod.run(cmd, cmdArgs, isHelpRequest);
224
+ break;
958
225
  }
959
- return;
960
- }
961
-
962
- if (sub === 'doctor') {
963
- // Run `headroom doctor` locally
964
- const { spawn } = await import('node:child_process');
965
- const child = spawn('headroom', ['doctor'], { stdio: 'inherit' });
966
- await new Promise((resolve) => child.on('close', resolve));
967
- return;
968
- }
969
-
970
- console.error(chalk.red(` ✗ Unknown headroom subcommand: ${sub}`));
971
- showHeadroomHelp();
972
- process.exit(1);
973
- }
974
-
975
- function showDashHelp() {
976
- console.log(`
977
- bizar dash — Manage the Bizar dashboard
978
-
979
- Usage:
980
- bizar dash <subcommand> [options] 
981
-
982
- Subcommands:
983
- start [--bg] [--port N] Start the dashboard (default port 4321)
984
- stop Stop the running dashboard
985
- status Show dashboard port and URL
986
- cleanup Find + kill zombie/orphan dashboards
987
- tui [--no-web] Launch the TUI
988
-
989
- Options:
990
- --bg Detach and run in background (for start)
991
- --port N Override the default port
992
- --no-web Skip launching the web UI (for tui)
993
-
994
- Examples:
995
- bizar dash start
996
- bizar dash start --bg
997
- bizar dash stop
998
- bizar dash status
999
- bizar dash tui
1000
- bizar dash tui --no-web
1001
-
1002
- Note:
1003
- \`bizar dashboard\` is a deprecated alias for \`bizar dash\` and still
1004
- works, but new code should use \`bizar dash\`.
1005
- `);
1006
- }
1007
-
1008
- function showMemoryHelp() {
1009
- console.log(`
1010
- memory <subcommand> Manage project memory (local-only or Git-shared Obsidian vault)
1011
- Subcommands: init, setup, status, link, unlink, write, pull, commit,
1012
- push, sync, reindex, conflicts, doctor
1013
- `);
1014
- }
1015
-
1016
- /**
1017
- * Detect whether the dashboard CLI is available.
1018
- * v4.0.0: primary path is the relative one (dashboard ships inside the
1019
- * same package at bizar-dash/src/cli.mjs). A global-npm fallback is
1020
- * kept for legacy users who still have @polderlabs/bizar-dash installed.
1021
- */
1022
- async function findBizarDash() {
1023
- // v4.0.0 — primary: relative import inside the same package
1024
- const primaryPath = join(import.meta.dirname, '..', 'bizar-dash', 'src', 'cli.mjs');
1025
- if (existsSync(primaryPath)) return primaryPath;
1026
-
1027
- // Legacy fallback: global npm install of @polderlabs/bizar-dash
1028
- const { execSync } = await import('node:child_process');
1029
- try {
1030
- const root = execSync('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
1031
- const dashPath = join(root, '@polderlabs', 'bizar-dash', 'src', 'cli.mjs');
1032
- if (existsSync(dashPath)) return dashPath;
1033
- } catch {
1034
- /* fall through */
1035
- }
1036
- // Local fallback — node_modules of this package
1037
- const localPath = fileURLToPath(new URL('../node_modules/@polderlabs/bizar-dash/src/cli.mjs', import.meta.url));
1038
- if (existsSync(localPath)) return localPath;
1039
- return null;
1040
- }
1041
226
 
1042
- /**
1043
- * Delegate a subcommand to the bizar-dash CLI, if installed.
1044
- */
1045
- async function delegateToDash(argsForDash) {
1046
- const dashPath = await findBizarDash();
1047
- if (!dashPath) {
1048
- console.log('The Bizar dashboard is part of this package.');
1049
- console.log('If you see this error, the install may be corrupted.');
1050
- console.log('Please report at: github.com/DrB0rk/BizarHarness/issues');
1051
- return;
1052
- }
1053
- const { spawn } = await import('node:child_process');
1054
- const child = spawn(process.execPath, [dashPath, ...argsForDash], {
1055
- stdio: 'inherit',
1056
- cwd: process.cwd(),
1057
- env: process.env,
1058
- });
1059
- await new Promise((resolve, reject) => {
1060
- child.on('exit', (code, signal) => {
1061
- if (code === 0) {
1062
- resolve();
1063
- return;
1064
- }
1065
- if (signal) {
1066
- reject(new Error(`dashboard exited via signal ${signal}`));
1067
- return;
1068
- }
1069
- reject(new Error(`dashboard exited with code ${code}`));
1070
- });
1071
- child.on('error', reject);
1072
- });
1073
- }
1074
-
1075
- function parseFlag(name) {
1076
- const idx = args.indexOf(name);
1077
- if (idx === -1) return null;
1078
- return args[idx + 1] || null;
1079
- }
1080
-
1081
- /**
1082
- * v4.4.11 — Parse `--with-mods <csv>` from the given subargs slice.
1083
- * Returns `null` if the flag isn't present (the provisioner's
1084
- * "don't touch mods" default), or a string[] of mod ids if it is.
1085
- */
1086
- function parseWithModsFlag(subargs) {
1087
- const idx = subargs.indexOf('--with-mods');
1088
- if (idx === -1) return null;
1089
- const raw = subargs[idx + 1];
1090
- if (!raw || raw.startsWith('--')) return [];
1091
- return raw
1092
- .split(',')
1093
- .map((s) => s.trim())
1094
- .filter(Boolean);
1095
- }
1096
-
1097
- async function readAutoLaunchWeb() {
1098
- try {
1099
- const fs = await import('node:fs');
1100
- const os = await import('node:os');
1101
- const path = await import('node:path');
1102
- const bizarConfigDir = process.platform === 'win32'
1103
- ? (process.env.APPDATA
1104
- ? path.join(process.env.APPDATA, 'bizar')
1105
- : path.join(os.homedir(), '.config', 'bizar'))
1106
- : (process.env.XDG_CONFIG_HOME
1107
- ? path.join(process.env.XDG_CONFIG_HOME, 'bizar')
1108
- : path.join(os.homedir(), '.config', 'bizar'));
1109
- const file = path.join(bizarConfigDir, 'settings.json');
1110
- if (!fs.existsSync(file)) return true;
1111
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
1112
- if (parsed && parsed.dashboard && typeof parsed.dashboard.autoLaunchWeb === 'boolean') {
1113
- return parsed.dashboard.autoLaunchWeb;
227
+ case 'artifact': {
228
+ const mod = await importCommand('artifact');
229
+ await mod.runArtifact(cmdArgs, { wantJson });
230
+ break;
1114
231
  }
1115
- } catch {
1116
- /* fall through */
1117
- }
1118
- return true;
1119
- }
1120
232
 
1121
- async function runTestGate() {
1122
- console.log(chalk.bold.hex('#a855f7')('\n ᚦ TEST GATE ᚦ\n'));
1123
- const { execSync } = await import('node:child_process');
1124
- const cwd = process.cwd();
1125
- const possible = [
1126
- { cmd: 'npm test', check: 'package.json' },
1127
- { cmd: 'pytest', check: 'pyproject.toml' },
1128
- { cmd: 'cargo test', check: 'Cargo.toml' },
1129
- { cmd: 'go test ./...', check: 'go.mod' },
1130
- ];
1131
- for (const suite of possible) {
1132
- try {
1133
- if (existsSync(join(cwd, suite.check))) {
1134
- console.log(` Running: ${suite.cmd}`);
1135
- execSync(suite.cmd, { stdio: 'inherit', timeout: 120000, cwd });
1136
- console.log('\n ✓ Test gate passed\n');
1137
- return true;
1138
- }
1139
- } catch {
1140
- console.log(`\n ✗ Test gate failed: ${suite.cmd}\n`);
1141
- process.exit(1);
233
+ case 'memory': {
234
+ const mod = await importCommand('memory');
235
+ await mod.run(cmd, cmdArgs, isHelpRequest);
236
+ break;
1142
237
  }
1143
- }
1144
- console.log(' No test suite detected. Install one to use the test gate.\n');
1145
- return false;
1146
- }
1147
238
 
1148
- /**
1149
- * Service commands — start / stop / status / logs / install / uninstall.
1150
- *
1151
- * Implementation lives in cli/service.mjs. The install / uninstall
1152
- * branches delegate to cli/service-controller.mjs for OS-level
1153
- * registration (systemd / launchd / schtasks).
1154
- */
1155
- async function runServiceCommand(sub) {
1156
- const { runService } = await import('./service.mjs');
1157
- await runService(sub || 'status', args.slice(2));
1158
- }
1159
-
1160
- async function main() {
1161
- if (args.includes('--check')) {
1162
- const status = checkSetupStatus();
1163
- console.log(JSON.stringify(status, null, 2));
1164
- process.exit(status.needed ? 1 : 0);
1165
- } else if (args.includes('--setup')) {
1166
- await ensureSetup({ silent: false });
1167
- process.exit(0);
1168
- } else if (args.includes('--postinstall')) {
1169
- // Legacy manual trigger — now an alias for --setup
1170
- await ensureSetup({ silent: false });
1171
- process.exit(0);
1172
- } else if (isVersionRequest) {
1173
- console.log(readCliVersion());
1174
- } else if (args[0] === 'audit') {
1175
- if (isHelpRequest) showAuditHelp();
1176
- else await runAudit();
1177
- } else if (args[0] === 'init') {
1178
- if (isHelpRequest) showInitHelp();
1179
- else await runInit(process.cwd());
1180
- } else if (args[0] === 'memory') {
1181
- if (isHelpRequest && !args[1]) showMemoryHelp();
1182
- else {
1183
- const { runMemory } = await import('./memory.mjs');
1184
- await runMemory(args[1], args.slice(2));
1185
- }
1186
- } else if (args[0] === 'headroom') {
1187
- await runHeadroomCommand(args.slice(1));
1188
- } else if (args[0] === 'export') {
1189
- if (isHelpRequest) showExportHelp();
1190
- else await runExport(parseFlag('--target'));
1191
- } else if (args[0] === 'test-gate') {
1192
- if (isHelpRequest) showTestGateHelp();
1193
- else await runTestGate();
1194
- } else if (args[0] === 'update') {
1195
- if (isHelpRequest) showUpdateHelp();
1196
- else {
1197
- // v4.4.11 — Same --with-mods opt-in for update.
1198
- const withMods = parseWithModsFlag(args.slice(1));
1199
- // runUpdate expects (subargs: string[], opts?: object). Splice
1200
- // --with-mods <csv> out of subargs since the provisioner now
1201
- // takes it via opts, not as a positional arg.
1202
- const subargs = args.slice(1).filter((a, i, arr) => {
1203
- if (a === '--with-mods') return false;
1204
- if (arr[i - 1] === '--with-mods') return false;
1205
- return true;
1206
- });
1207
- await runUpdate(subargs, { withMods });
1208
- }
1209
- } else if (args[0] === 'dev-link') {
1210
- if (isHelpRequest) showDevLinkHelp();
1211
- else {
1212
- const { createDevLink } = await import('./dev-link.mjs');
1213
- const positional = args.slice(1).filter((a) => !a.startsWith('-'));
1214
- const flags = args.slice(1).filter((a) => a.startsWith('-'));
1215
- const sourceDir = positional[0] ?? null;
1216
- const force = flags.includes('--force') || flags.includes('-f');
1217
- const ok = createDevLink(sourceDir, { force });
1218
- if (!ok) process.exit(1);
1219
- }
1220
- } else if (args[0] === 'dev-unlink') {
1221
- if (isHelpRequest) showDevLinkHelp();
1222
- else {
1223
- const { removeDevLink } = await import('./dev-link.mjs');
1224
- const force = args.includes('--force') || args.includes('-f');
1225
- const ok = await removeDevLink({ force });
1226
- if (!ok) process.exit(1);
1227
- }
1228
- } else if (args[0] === 'doctor') {
1229
- if (isHelpRequest) showDoctorHelp();
1230
- else {
1231
- const { runDoctor } = await import('./doctor.mjs');
1232
- const result = await runDoctor();
1233
- if (result.failed > 0) process.exit(1);
239
+ case 'usage': {
240
+ const mod = await importCommand('usage');
241
+ await mod.run(cmd, cmdArgs, isHelpRequest);
242
+ break;
1234
243
  }
1235
- } else if (args[0] === 'repair') {
1236
- // v4.4.3 — One-shot repair for stale `bizar` bin symlinks.
1237
- // Symptom: after `npm i -g @polderlabs/bizar`, the package is
1238
- // installed under `npm root -g` but the `bizar` symlink on PATH
1239
- // still points at a legacy install path (e.g. ~/.local/lib/...).
1240
- // Calls of `bizar ...` then run the old code with the old bugs.
1241
- if (isHelpRequest) {
1242
- console.log(`
1243
- bizar repair — Fix common install issues
1244
244
 
1245
- Usage:
1246
- bizar repair Diagnose + fix stale bin symlinks and mismatched versions
1247
- bizar repair --dry-run Show what would change without modifying anything
1248
- bizar repair --bin-only Only fix the bin symlink; skip version checks
1249
- `);
1250
- } else {
1251
- const { runRepair } = await import('./repair.mjs');
1252
- const dryRun = args.includes('--dry-run');
1253
- const binOnly = args.includes('--bin-only');
1254
- const result = await runRepair({ dryRun, binOnly });
1255
- if (!result.ok) {
1256
- for (const n of result.notes) console.log(` ${n}`);
1257
- process.exit(1);
245
+ // All other utility commands
246
+ case 'audit':
247
+ case 'init':
248
+ case 'export':
249
+ case 'test-gate':
250
+ case 'dev-link':
251
+ case 'dev-unlink':
252
+ case 'doctor':
253
+ case 'repair':
254
+ case 'heads-up':
255
+ case 'bg':
256
+ case 'browser-harness-up':
257
+ case 'providers':
258
+ case 'plan': {
259
+ const mod = await importCommand('util');
260
+ const found = await mod.run(cmd, cmdArgs, isHelpRequest);
261
+ if (!found) {
262
+ console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
263
+ showHelp();
264
+ process.exit(EXIT_ERROR);
1258
265
  }
1259
- for (const n of result.notes) console.log(` ${n}`);
1260
- if (result.fixed.length > 0) {
1261
- console.log(chalk.green('\n Repair complete. Re-run your shell or `hash -r` to pick up the new path.'));
1262
- }
1263
- }
1264
- } else if (args[0] === 'heads-up') {
1265
- // v3.21.0 — Pre-push / pre-release heads-ups
1266
- await runHeadsUp(args[1], args.slice(2));
1267
- } else if (args[0] === 'plan') {
1268
- await runArtifact(args.slice(1), {});
1269
- } else if (args[0] === 'install') {
1270
- if (isHelpRequest) showInstallHelp();
1271
- else {
1272
- // v4.4.11 — Parse --with-mods <csv> to opt into mod installs
1273
- // during the run. Default: mods are NEVER touched.
1274
- const withMods = parseWithModsFlag(args.slice(1));
1275
- await runInstaller({ withMods });
1276
- // v4.4.3 — After install, repair any stale bin symlinks so the
1277
- // user picks up the new code (the installer itself may have
1278
- // been running from a stale install path).
1279
- try {
1280
- const { runRepair } = await import('./repair.mjs');
1281
- const r = await runRepair({});
1282
- if (r.fixed.length > 0) {
1283
- console.log(chalk.cyan('\n Repair: repointed stale bin symlinks:'));
1284
- for (const f of r.fixed) console.log(` ${f}`);
1285
- console.log(chalk.dim(' Re-run your shell or `hash -r` to pick up the new path.'));
1286
- }
1287
- } catch (err) {
1288
- console.log(chalk.dim(` Repair skipped: ${err.message}`));
1289
- }
1290
- }
1291
- } else if (args[0] === 'service') {
1292
- if (isHelpRequest) showServiceHelp();
1293
- else await runServiceCommand(args[1]);
1294
- } else if (args[0] === 'bg') {
1295
- // v3.11.1 — Background agent manager (list / view / kill / logs).
1296
- const { runBg } = await import('./bg.mjs');
1297
- await runBg(args[1], args.slice(2));
1298
- } else if (args[0] === 'browser-harness-up') {
1299
- // v3.20.7 — Browser-harness daemon: start / stop / status / restart
1300
- // Chromium with remote debugging so the browser-harness Python tool
1301
- // (from https://github.com/browser-use/browser-harness) can connect.
1302
- const { execFileSync } = await import('node:child_process');
1303
- const sub = args[1] || 'start';
1304
- const scriptPath = join(import.meta.dirname || process.cwd(), 'browser-harness-up.sh');
1305
- try {
1306
- const out = execFileSync('bash', [scriptPath, sub], {
1307
- encoding: 'utf8',
1308
- stdio: 'inherit',
1309
- });
1310
- if (out) process.stdout.write(out);
1311
- } catch (err) {
1312
- console.error(chalk.red(` ✗ browser-harness-up ${sub} failed (exit ${err.status ?? 1})`));
1313
- process.exit(err.status || 1);
1314
- }
1315
- } else if (args[0] === 'providers' && args[1] === 'detect') {
1316
- // v3.16.0 — Auto-detect provider API keys from env + opencode.json.
1317
- const { runProvidersDetect } = await import('./providers-detect.mjs');
1318
- await runProvidersDetect(args.slice(2));
1319
- } else if (args[0] === 'mod') {
1320
- // v3.20.5 — Mod manager. Talks to the running dashboard over HTTP
1321
- // (the dashboard owns the actual mod install/upgrade logic and the
1322
- // opencode-config install/uninstall of instruction files).
1323
- await runModCommand(args.slice(1));
1324
- } else if (args[0] === 'minimax') {
1325
- // v4.5.0 — MiniMax Token Plan integration CLI. Status, remains,
1326
- // test, config, clear, reset-onboarding. All commands shell out
1327
- // to the dashboard's /api/minimax/* routes.
1328
- await runMinimaxCommand(args.slice(1));
1329
- } else if (args[0] === 'usage') {
1330
- // v4.6.0 — Compact usage analytics summary from the JSONL store.
1331
- await runUsageCommand(args.slice(1));
1332
- } else if (args[0] === 'dash' || args[0] === 'dashboard') {
1333
- // `bizar dashboard` is a deprecated alias for `bizar dash`
1334
- if (args[0] === 'dashboard') {
1335
- console.warn(chalk.yellow(' ⚠ `bizar dashboard` is deprecated, use `bizar dash` instead.'));
1336
- }
1337
- const dashArgs = args.slice(1); // everything after 'dash' or 'dashboard'
1338
- if (dashArgs.length === 0 || isHelpRequest) {
1339
- showDashHelp();
1340
- } else {
1341
- await runDash(dashArgs);
266
+ break;
1342
267
  }
1343
- } else if (isHelpRequest) {
1344
- showHelp();
1345
- } else {
1346
- // No args — show help (breaking: previously launched TUI)
1347
- showHelp();
1348
- process.exit(1);
1349
- }
1350
- }
1351
-
1352
- // ── Dashboard subcommand ───────────────────────────────────────────────────────
1353
268
 
1354
- /**
1355
- * Parse dash-specific options from an array of args.
1356
- * Returns { opts, remaining } where opts have --bg / --port stripped.
1357
- */
1358
- function parseDashOpts(dashArgs) {
1359
- const opts = { bg: false, port: null, noWeb: false };
1360
- const remaining = [];
1361
- for (let i = 0; i < dashArgs.length; i++) {
1362
- const a = dashArgs[i];
1363
- if (a === '--bg') {
1364
- opts.bg = true;
1365
- } else if (a === '--no-web') {
1366
- opts.noWeb = true;
1367
- } else if (a === '--port' && i + 1 < dashArgs.length) {
1368
- opts.port = Number(dashArgs[i + 1]);
1369
- i++;
1370
- } else {
1371
- remaining.push(a);
269
+ default: {
270
+ console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
271
+ showHelp();
272
+ process.exit(EXIT_ERROR);
1372
273
  }
1373
274
  }
1374
- return { opts, remaining };
1375
275
  }
1376
276
 
1377
- /**
1378
- * Try to load the dashboard CLI module.
1379
- * v4.0.0: the dashboard ships inside this package at bizar-dash/src/cli.mjs.
1380
- * A legacy npm-global fallback is kept for the transitional period.
1381
- */
1382
- async function loadDashCli() {
1383
- const { pathToFileURL } = await import('node:url');
1384
- const { join } = await import('node:path');
1385
-
1386
- const primary = join(import.meta.dirname, '..', 'bizar-dash', 'src', 'cli.mjs');
277
+ // ── Run ───────────────────────────────────────────────────────────────────────
1387
278
 
1388
- // Legacy npm-global fallback (only computed if primary is missing)
1389
- let legacyFallbacks = [];
1390
- try {
1391
- const { execFileSync } = await import('node:child_process');
1392
- const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000 }).trim();
1393
- if (npmRoot) legacyFallbacks = [join(npmRoot, '@polderlabs', 'bizar-dash', 'src', 'cli.mjs')];
1394
- } catch { /* npm not available or no globals — fine */ }
1395
-
1396
- const candidates = [primary, ...legacyFallbacks];
1397
-
1398
- for (const p of candidates) {
1399
- try {
1400
- const url = pathToFileURL(p).href;
1401
- const mod = await import(url);
1402
- return mod;
1403
- } catch (_e) {
1404
- // try next
1405
- }
1406
- }
1407
- return null;
1408
- }
1409
-
1410
- /**
1411
- * Dispatch a dashboard subcommand by loading the dashboard module and
1412
- * calling the appropriate exported function.
1413
- */
1414
- async function runDash(dashArgs) {
1415
- const { opts, remaining } = parseDashOpts(dashArgs);
1416
- const sub = remaining[0];
1417
- const subOpts = { ...opts, subArgs: remaining.slice(1) };
1418
-
1419
- const dashModule = await loadDashCli();
1420
- if (!dashModule) {
1421
- console.error(chalk.red(' ✗ Dashboard not found.'));
1422
- console.error(chalk.dim(' this should not happen — please report a bug at github.com/DrB0rk/BizarHarness/issues'));
1423
- process.exit(1);
1424
- }
1425
-
1426
- switch (sub) {
1427
- case 'start':
1428
- if (subOpts.bg) {
1429
- // v4.4.0 — Background mode: spawn the dashboard as a detached
1430
- // child process via dashModule.startInBackground(). The child
1431
- // runs `node cli.mjs start --bg` which keeps itself alive via
1432
- // the bg-* pollers and the (v4.4.3) module-scope backgroundHandle.
1433
- await dashModule.startInBackground(['start', ...(subOpts.subArgs || [])]);
1434
- } else {
1435
- await dashModule.start(subOpts);
1436
- }
1437
- break;
1438
- case 'stop':
1439
- await dashModule.stop(subOpts);
1440
- break;
1441
- case 'status':
1442
- await dashModule.status(subOpts);
1443
- break;
1444
- case 'cleanup':
1445
- // v3.20.7 — Find + kill zombie/orphan dashboards.
1446
- // Pass through --force and --kill flags to the dash module.
1447
- await dashModule.cleanup(subOpts);
1448
- break;
1449
- case 'tui':
1450
- await dashModule.tui(subOpts);
1451
- break;
1452
- default:
1453
- console.error(chalk.red(` ✗ Unknown subcommand: ${sub}`));
1454
- showDashHelp();
1455
- process.exit(1);
1456
- }
279
+ const thisFile = fileURLToPath(import.meta.url);
280
+ const isMainModule = process.argv[1] === thisFile;
281
+ if (isMainModule) {
282
+ await main().catch((err) => {
283
+ console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
284
+ process.exit(EXIT_ERROR);
285
+ });
1457
286
  }
1458
-
1459
- // ── Main ──────────────────────────────────────────────────────────────────────
1460
-
1461
- await main().catch((err) => {
1462
- console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
1463
- process.exit(1);
1464
- });