@polderlabs/bizar 4.5.2 → 4.7.1

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