@polderlabs/bizar 5.4.0 → 5.5.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 (99) hide show
  1. package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
  2. package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
  5. package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
  6. package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
  7. package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
  8. package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
  10. package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
  11. package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
  12. package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
  13. package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
  14. package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
  15. package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
  16. package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
  17. package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
  18. package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
  19. package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
  20. package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
  21. package/bizar-dash/dist/index.html +8 -8
  22. package/bizar-dash/dist/mobile.html +4 -4
  23. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  24. package/bizar-dash/src/server/backup-store.mjs +6 -1
  25. package/bizar-dash/src/server/bg-spawner.mjs +574 -0
  26. package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
  27. package/bizar-dash/src/server/memory-store.mjs +43 -3
  28. package/bizar-dash/src/server/routes/background.mjs +143 -0
  29. package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
  30. package/bizar-dash/src/server/routes/memory.mjs +8 -0
  31. package/bizar-dash/src/server/server.mjs +22 -0
  32. package/bizar-dash/src/web/App.tsx +3 -0
  33. package/bizar-dash/src/web/MobileApp.tsx +1 -10
  34. package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
  35. package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
  36. package/bizar-dash/src/web/lib/types.ts +39 -1
  37. package/bizar-dash/src/web/styles/main.css +43 -0
  38. package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
  39. package/bizar-dash/src/web/styles/settings.css +46 -0
  40. package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
  41. package/bizar-dash/src/web/views/Settings.tsx +12 -12
  42. package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
  43. package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
  44. package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
  45. package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
  46. package/bizar-dash/tests/background-steer.test.mjs +42 -0
  47. package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
  48. package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
  49. package/bizar-dash/tests/memory-cli.test.mjs +1 -1
  50. package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
  51. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
  52. package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
  53. package/bizar-dash/tests/settings-layout.test.tsx +3 -3
  54. package/bizar-dash/tests/settings-nav.test.tsx +7 -5
  55. package/cli/bin.mjs +16 -0
  56. package/cli/commands/deploy/docker.mjs +2 -1
  57. package/cli/commands/lightrag.mjs +171 -0
  58. package/cli/commands/service.mjs +7 -0
  59. package/cli/commands/util.mjs +8 -1
  60. package/cli/init.mjs +1 -1
  61. package/cli/post-install-smoke.mjs +230 -0
  62. package/cli/provision.mjs +177 -0
  63. package/cli/provision.test.mjs +180 -0
  64. package/cli/service-controller.mjs +378 -28
  65. package/cli/service-controller.test.mjs +100 -0
  66. package/cli/service-env.mjs +139 -0
  67. package/cli/service.mjs +23 -0
  68. package/config/agents/_shared/AGENT_BASELINE.md +6 -6
  69. package/install.sh +87 -6
  70. package/package.json +1 -1
  71. package/plugins/bizar/index.ts +147 -0
  72. package/plugins/bizar/src/background-state.ts +84 -5
  73. package/plugins/bizar/src/background.ts +388 -10
  74. package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
  75. package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
  76. package/plugins/bizar/src/opencode-runner.ts +87 -1
  77. package/plugins/bizar/src/tools/bg-pause.ts +77 -0
  78. package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
  79. package/plugins/bizar/src/tools/bg-resume.ts +72 -0
  80. package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
  81. package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
  82. package/plugins/bizar/src/tools/bg-status.ts +10 -0
  83. package/plugins/bizar/src/tools/memory-list.ts +135 -0
  84. package/plugins/bizar/src/tools/memory-read.ts +142 -0
  85. package/plugins/bizar/src/tools/memory-search.ts +228 -0
  86. package/plugins/bizar/src/tools/memory-write.ts +183 -0
  87. package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
  88. package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
  89. package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
  90. package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
  91. package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
  92. package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
  93. package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
  94. package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
  95. package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
  96. package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
  97. package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
  98. package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
  99. package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
@@ -293,4 +293,40 @@ describe('MobileLayout', () => {
293
293
  );
294
294
  expect(screen.getByTestId('child')).toBeInTheDocument();
295
295
  });
296
+
297
+ it('layout has correct CSS class applied', () => {
298
+ const { container } = render(
299
+ <MobileLayout
300
+ activeTab="activity"
301
+ onTabChange={vi.fn()}
302
+ tabs={TABS}
303
+ fullTabs={TABS}
304
+ >
305
+ <div>Child content</div>
306
+ </MobileLayout>,
307
+ );
308
+ // Verify the layout and content elements exist with correct class names
309
+ const layout = container.querySelector('.mobile-layout');
310
+ expect(layout).toBeInTheDocument();
311
+ const main = container.querySelector('.mobile-content');
312
+ expect(main).toBeInTheDocument();
313
+ const header = container.querySelector('.mobile-header');
314
+ expect(header).toBeInTheDocument();
315
+ });
316
+
317
+ it('renders exactly one header and one main inside the layout', () => {
318
+ const { container } = render(
319
+ <MobileLayout
320
+ activeTab="activity"
321
+ onTabChange={vi.fn()}
322
+ tabs={TABS}
323
+ fullTabs={TABS}
324
+ >
325
+ <div>Child content</div>
326
+ </MobileLayout>,
327
+ );
328
+ const layout = container.querySelector('.mobile-layout');
329
+ expect(layout!.querySelectorAll('header').length).toBe(1);
330
+ expect(layout!.querySelectorAll('main').length).toBe(1);
331
+ });
296
332
  });
@@ -57,7 +57,7 @@ describe('Sidebar — settingsMode', () => {
57
57
  // Should NOT show the normal tab rail
58
58
  expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
59
59
  // Should show the back button
60
- expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument();
60
+ expect(screen.getByRole('button', { name: /exit settings/i })).toBeInTheDocument();
61
61
  // Should show section groups (use selector to avoid duplicate text matches)
62
62
  expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
63
63
  expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
@@ -109,8 +109,8 @@ describe('Sidebar — settingsMode', () => {
109
109
  onExitSettings={onExitSettings}
110
110
  />,
111
111
  );
112
- await user.click(screen.getByRole('button', { name: /memory/i }));
113
- expect(onSettingsSectionChange).toHaveBeenCalledWith('memory');
112
+ await user.click(screen.getByRole('button', { name: /env vars/i }));
113
+ expect(onSettingsSectionChange).toHaveBeenCalledWith('env-vars');
114
114
  });
115
115
 
116
116
  it('calls onTabChange when a normal sidebar tab is clicked', async () => {
@@ -25,7 +25,7 @@ describe('SettingsNav', () => {
25
25
  onExitSettings={onExitSettings}
26
26
  />,
27
27
  );
28
- expect(screen.getByRole('button', { name: /back/i })).toBeInTheDocument();
28
+ expect(screen.getByRole('button', { name: /exit settings/i })).toBeInTheDocument();
29
29
  });
30
30
 
31
31
  it('calls onExitSettings when back button is clicked', async () => {
@@ -52,7 +52,7 @@ describe('SettingsNav', () => {
52
52
  expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
53
53
  expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
54
54
  expect(screen.getByText('Experience', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
55
- expect(screen.getByText('Data', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
55
+ // Data group was removed - no longer exists in SettingsNav
56
56
  });
57
57
 
58
58
  it('renders section items within each group', () => {
@@ -63,14 +63,16 @@ describe('SettingsNav', () => {
63
63
  onExitSettings={onExitSettings}
64
64
  />,
65
65
  );
66
+ // General group
66
67
  expect(screen.getByRole('button', { name: /theme/i })).toBeInTheDocument();
68
+ expect(screen.getByRole('button', { name: /layout/i })).toBeInTheDocument();
69
+ expect(screen.getByRole('button', { name: /general/i })).toBeInTheDocument();
70
+ // Core group
67
71
  expect(screen.getByRole('button', { name: /env vars/i })).toBeInTheDocument();
68
- expect(screen.getByRole('button', { name: /memory/i })).toBeInTheDocument();
69
72
  expect(screen.getByRole('button', { name: /system llm/i })).toBeInTheDocument();
73
+ // Experience group
70
74
  expect(screen.getByRole('button', { name: /updates/i })).toBeInTheDocument();
71
- expect(screen.getByRole('button', { name: /skills/i })).toBeInTheDocument();
72
75
  expect(screen.getByRole('button', { name: /headroom/i })).toBeInTheDocument();
73
- expect(screen.getByRole('button', { name: /backup/i })).toBeInTheDocument();
74
76
  });
75
77
 
76
78
  it('calls onSectionChange with correct id when a section is clicked', async () => {
package/cli/bin.mjs CHANGED
@@ -101,6 +101,7 @@ function showHelp() {
101
101
  dash <subcommand> Manage the dashboard (start/stop/status/cleanup/tui)
102
102
  memory <subcommand> Manage project memory (Bizar Memory Service)
103
103
  headroom <subcommand> Manage Headroom context compression
104
+ lightrag <subcommand> Manage the LightRAG knowledge-graph server (status/start/autostart)
104
105
  minimax <subcommand> Manage MiniMax Token Plan integration
105
106
  tailscale <subcommand> Manage Tailscale integration (auth, serve, status)
106
107
  mod <subcommand> Manage mods (install/upgrade/list via the dashboard API)
@@ -284,6 +285,21 @@ async function main() {
284
285
  break;
285
286
  }
286
287
 
288
+ case 'lightrag': {
289
+ // v5.x — LightRAG management CLI (issue #6).
290
+ // Subcommands: status, start, autostart.
291
+ const mod = await importCommand('lightrag');
292
+ if (!mod) {
293
+ console.error(chalk.red(` ✗ Could not load lightrag command module`));
294
+ process.exit(EXIT_ERROR);
295
+ return;
296
+ }
297
+ dbg('loaded command module:', 'lightrag');
298
+ await mod.run(cmd, cmdArgs, isHelpRequest);
299
+ dbg('command returned:', cmd);
300
+ break;
301
+ }
302
+
287
303
  case 'mod': {
288
304
  const mod = await importCommand('mod');
289
305
  if (!mod) {
@@ -46,7 +46,8 @@ function generateComposeScaffold(outDir, opts) {
46
46
  ' - .env',
47
47
  ' volumes:',
48
48
  ' - bizar-config:/home/bizar/.config/bizar',
49
- ' - bizar-memory:/home/bizar/.local/share/bizar/memory',
49
+ // v5.x — default memory vault relocated to ~/.bizar_memory
50
+ ' - bizar-memory:/home/bizar/.bizar_memory',
50
51
  '',
51
52
  'volumes:',
52
53
  ' bizar-config:',
@@ -0,0 +1,171 @@
1
+ /**
2
+ * cli/commands/lightrag.mjs
3
+ *
4
+ * v5.x — LightRAG management CLI (issue #6).
5
+ *
6
+ * Subcommands:
7
+ * autostart Trigger the dashboard's lightragStartupHook() against
8
+ * the current working directory's project. Used by
9
+ * `install.sh` and by operators who want to manually
10
+ * start LightRAG without opening the dashboard.
11
+ * start Alias for `autostart` (matches `headroom start`).
12
+ * status Print whether the LightRAG server is running.
13
+ *
14
+ * The dashboard server already auto-invokes `lightragStartupHook()` on
15
+ * boot, so this CLI is mostly for ops/debugging. The `autostart` form is
16
+ * the one called by `install.sh` and `bizar update` flows.
17
+ */
18
+ import chalk from 'chalk';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { process } from 'node:process';
22
+
23
+ export function showLightragHelp() {
24
+ console.log(`
25
+ bizar lightrag — Manage the LightRAG knowledge graph server
26
+
27
+ Usage:
28
+ bizar lightrag status Show whether LightRAG is running
29
+ bizar lightrag start Start LightRAG for the current project
30
+ bizar lightrag autostart Same as \`start\` — runs the startup hook
31
+ bizar lightrag autostart --no-dashboard Run hook without touching the
32
+ running dashboard (skips status check)
33
+
34
+ Description:
35
+ LightRAG is the knowledge-graph indexer for the project's memory vault.
36
+ The dashboard server auto-starts LightRAG on boot (when
37
+ .bizar/memory.json#lightrag.enabled is true). Use this command to:
38
+ - manually trigger the startup hook against the current project
39
+ - check whether the LightRAG server is up
40
+
41
+ Environment:
42
+ BIZAR_LIGHTRAG_AUTOSTART=0|false|no Disable auto-start (default: enabled)
43
+ BIZAR_MEMORY_VAULT=/path/to/vault Override the memory vault location
44
+ `);
45
+ }
46
+
47
+ /**
48
+ * Resolve the project root for the lightrag command. We use `process.cwd()`
49
+ * — the user is expected to run this from inside a Bizar project. If
50
+ * `.bizar/memory.json` is missing, we still pass the cwd through and let
51
+ * the hook's `resolveLightRAGConfig` fall back to defaults.
52
+ */
53
+ function resolveProjectRoot() {
54
+ return process.cwd();
55
+ }
56
+
57
+ async function runLightragCommand(lightragArgs) {
58
+ const sub = lightragArgs[0];
59
+ const flags = new Set(lightragArgs.slice(1).filter((a) => a.startsWith('-')));
60
+
61
+ if (!sub || sub === '--help' || sub === '-h' || flags.has('--help') || flags.has('-h')) {
62
+ showLightragHelp();
63
+ return;
64
+ }
65
+
66
+ if (sub === 'autostart' || sub === 'start') {
67
+ // Lazy import so the help text doesn't pay the cost of pulling in
68
+ // memory-lightrag (and its transitive deps) on every command.
69
+ const { lightragStartupHook, isRunning, resolveLightRAGConfig } = await import('../../bizar-dash/src/server/memory-lightrag.mjs');
70
+ const projectRoot = resolveProjectRoot();
71
+ console.log(chalk.dim(` lightrag autostart: project=${projectRoot}`));
72
+
73
+ // If the dashboard is running, prefer its endpoint (it owns the
74
+ // server's lifecycle). Fall back to a direct hook call otherwise.
75
+ if (!flags.has('--no-dashboard')) {
76
+ try {
77
+ const dashboardResult = await callDashboardAutostart();
78
+ if (dashboardResult && dashboardResult.started) {
79
+ console.log(chalk.green(` ✓ lightrag auto-started via dashboard (pid=${dashboardResult.pid || '?'})`));
80
+ return;
81
+ }
82
+ if (dashboardResult && dashboardResult.ok && !dashboardResult.started) {
83
+ console.log(chalk.dim(` · lightrag: ${dashboardResult.reason || 'already running'}`));
84
+ return;
85
+ }
86
+ } catch (err) {
87
+ // Dashboard unreachable — fall through to direct call. Common
88
+ // case: user ran `bizar lightrag autostart` from a fresh shell
89
+ // without the dashboard running.
90
+ console.log(chalk.dim(` · dashboard not reachable, running hook locally: ${err?.message || err}`));
91
+ }
92
+ }
93
+
94
+ const r = await lightragStartupHook(projectRoot);
95
+ if (r.ok && r.started) {
96
+ console.log(chalk.green(` ✓ lightrag started (pid=${r.pid || '?'})`));
97
+ } else if (r.ok) {
98
+ console.log(chalk.dim(` · lightrag: ${r.reason || 'not started'}`));
99
+ } else {
100
+ console.log(chalk.yellow(` ! lightrag autostart: ${r.error || r.reason || 'failed'}`));
101
+ process.exitCode = 1;
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (sub === 'status') {
107
+ const { isRunning, resolveLightRAGConfig } = await import('../../bizar-dash/src/server/memory-lightrag.mjs');
108
+ const projectRoot = resolveProjectRoot();
109
+ const cfg = resolveLightRAGConfig(projectRoot);
110
+ const running = await isRunning(cfg);
111
+ console.log('');
112
+ console.log(chalk.bold(' LightRAG status'));
113
+ console.log('');
114
+ console.log(` ${chalk.dim('running:')} ${running ? chalk.green('yes') : chalk.yellow('no')}`);
115
+ console.log(` ${chalk.dim('host:')} ${cfg.host}:${cfg.port}`);
116
+ console.log(` ${chalk.dim('enabled:')} ${cfg.enabled ? chalk.green('yes') : chalk.yellow('no')}`);
117
+ console.log(` ${chalk.dim('workingDir:')} ${cfg.workingDir}`);
118
+ console.log(` ${chalk.dim('llmBinding:')} ${cfg.llmBinding} (${cfg.llmModel})`);
119
+ console.log(` ${chalk.dim('embedding:')} ${cfg.embeddingBinding} (${cfg.embeddingModel})`);
120
+ console.log('');
121
+ return;
122
+ }
123
+
124
+ console.error(chalk.red(` ✗ Unknown lightrag subcommand: ${sub}`));
125
+ showLightragHelp();
126
+ process.exit(1);
127
+ }
128
+
129
+ /**
130
+ * Call the dashboard's startup-hook endpoint if the dashboard is up.
131
+ * Returns the JSON body on success, null if the dashboard is unreachable
132
+ * (so the caller can fall back to a direct hook call).
133
+ */
134
+ async function callDashboardAutostart() {
135
+ // Read dashboard connection from the same files the other CLI commands use.
136
+ const HOME = process.env.HOME || '/';
137
+ const portFile = join(HOME, '.config', 'bizar', 'dashboard.port');
138
+ const authFile = join(HOME, '.config', 'bizar', 'dash-auth.json');
139
+ let port = 4321;
140
+ let secret = '';
141
+ try {
142
+ if (existsSync(portFile)) {
143
+ const parsed = parseInt(readFileSync(portFile, 'utf8').trim(), 10);
144
+ if (Number.isFinite(parsed) && parsed > 0) port = parsed;
145
+ }
146
+ } catch { /* ignore */ }
147
+ try {
148
+ if (existsSync(authFile)) {
149
+ const raw = JSON.parse(readFileSync(authFile, 'utf8'));
150
+ secret = raw?.password || '';
151
+ }
152
+ } catch { /* ignore */ }
153
+ const url = `http://127.0.0.1:${port}/api/lightrag/autostart`;
154
+ const headers = { 'content-type': 'application/json', accept: 'application/json' };
155
+ if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
156
+ const res = await fetch(url, {
157
+ method: 'POST',
158
+ headers,
159
+ body: JSON.stringify({}),
160
+ signal: AbortSignal.timeout(2000),
161
+ });
162
+ if (!res.ok) {
163
+ return null;
164
+ }
165
+ const text = await res.text();
166
+ try { return text ? JSON.parse(text) : null; } catch { return null; }
167
+ }
168
+
169
+ export async function run(name, args, isHelpRequest) {
170
+ await runLightragCommand(args);
171
+ }
@@ -35,6 +35,8 @@ export function showServiceHelp() {
35
35
  bizar service follow Follow the service log until Ctrl-C
36
36
  bizar service install Register with systemd / launchd / scheduled task
37
37
  bizar service install --force Re-install even when the unit matches
38
+ bizar service restart Stop, reinstall unit, start (used by update)
39
+ bizar service restart --force Reinstall the unit before starting
38
40
  bizar service uninstall Remove the OS-level autostart
39
41
  bizar service uninstall --force Force-uninstall even when nothing is registered
40
42
 
@@ -50,6 +52,11 @@ export function showServiceHelp() {
50
52
  install, a normal user does not need to run \`bizar service start\`
51
53
  for the dashboard background process — the OS does it at login.
52
54
 
55
+ restart stops the running daemon, reinstalls the unit (so the
56
+ freshly-installed code is loaded), and starts it again. This is
57
+ what \`bizar update\` uses to pick up the new binary without leaving
58
+ the old one running.
59
+
53
60
  Tailscale Integration (v5.2):
54
61
  TAILSCALE_AUTHKEY=${tsAuthkey}
55
62
  BIZAR_TAILSCALE_AUTOSETUP=${tsAuto}
@@ -265,8 +265,15 @@ export async function run(name, args, isHelpRequest) {
265
265
  case 'doctor':
266
266
  if (isHelpRequest) showDoctorHelp();
267
267
  else {
268
- const { runDoctor } = await import('../doctor.mjs');
269
268
  const wantJson = args.includes('--json');
269
+ if (args[0] === 'smoke') {
270
+ // `bizar doctor smoke` — run post-install smoke test
271
+ const { runSmokeTest } = await import('../post-install-smoke.mjs');
272
+ const result = await runSmokeTest();
273
+ if (wantJson) process.stdout.write(JSON.stringify(result) + '\n');
274
+ process.exit(result.ok ? 0 : 1);
275
+ }
276
+ const { runDoctor } = await import('../doctor.mjs');
270
277
  const result = await runDoctor({ silent: wantJson, json: wantJson });
271
278
  if (wantJson) process.stdout.write(JSON.stringify(result) + '\n');
272
279
  if (result.failed > 0) process.exit(1);
package/cli/init.mjs CHANGED
@@ -260,7 +260,7 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
260
260
  name: 'memoryMode',
261
261
  message: 'Memory backend',
262
262
  choices: [
263
- { name: 'managed (shared user-level repo at ~/.local/share/bizar/memory/bizar-memory/)', value: 'managed' },
263
+ { name: 'managed (shared user-level repo at ~/.bizar_memory/bizar-memory/)', value: 'managed' },
264
264
  { name: 'local-only (vault stays in this project at .obsidian/)', value: 'local-only' },
265
265
  ],
266
266
  default: 'managed',
@@ -0,0 +1,230 @@
1
+ /**
2
+ * cli/post-install-smoke.mjs
3
+ *
4
+ * v5.x — Post-install smoke test for BizarHarness.
5
+ *
6
+ * Verifies the installation is functional by checking:
7
+ * 1. Memory vault exists and is git-initialized
8
+ * 2. `bizar doctor` exits 0
9
+ * 3. Dashboard HTTP responds 200
10
+ * 4. WebSocket connects to dashboard
11
+ * 5. `bizar bg list` returns (even if empty)
12
+ * 6. lightrag-server --version works
13
+ *
14
+ * Each check has a 30s timeout. Exits 0 if all pass, 1 otherwise.
15
+ *
16
+ * Public API:
17
+ * runSmokeTest({ bizarHome, repoPath })
18
+ * → Promise<{ ok: boolean, checks: CheckResult[] }>
19
+ *
20
+ * CheckResult: { name: string, ok: boolean, message: string }
21
+ */
22
+
23
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
24
+ import { homedir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { execSync } from 'node:child_process';
27
+ import { execFileSync } from 'node:child_process';
28
+
29
+ const DEFAULT_TIMEOUT_MS = 30_000;
30
+ const HOME = homedir();
31
+ const DEFAULT_BIZAR_HOME = join(HOME, '.config', 'bizar');
32
+ const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
33
+ const DEFAULT_DASHBOARD_PORT = 4097;
34
+
35
+ /**
36
+ * @param {object} opts
37
+ * @param {string} [opts.bizarHome]
38
+ * @param {string} [opts.repoPath]
39
+ * @param {number} [opts.timeoutMs]
40
+ */
41
+ export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
42
+ const bh = bizarHome || DEFAULT_BIZAR_HOME;
43
+ const mv = process.env.BIZAR_MEMORY_VAULT || DEFAULT_MEMORY_VAULT;
44
+ const port = parseInt(process.env.BIZAR_DASHBOARD_PORT || String(DEFAULT_DASHBOARD_PORT), 10) || DEFAULT_DASHBOARD_PORT;
45
+ const checks = [];
46
+
47
+ function check(name, fn) {
48
+ try {
49
+ const result = fn();
50
+ checks.push({ name, ok: result.ok, message: result.message || '' });
51
+ } catch (err) {
52
+ checks.push({ name, ok: false, message: err instanceof Error ? err.message : String(err) });
53
+ }
54
+ }
55
+
56
+ // ── 1. Memory vault exists and is git-initialised ───────────────────────
57
+ check('memory vault exists', () => {
58
+ if (!existsSync(mv)) {
59
+ return { ok: false, message: `${mv} does not exist` };
60
+ }
61
+ const gitDir = join(mv, '.git');
62
+ if (!existsSync(gitDir)) {
63
+ return { ok: false, message: `${mv} is not git-initialized (run: git init)` };
64
+ }
65
+ return { ok: true, message: `${mv} present and git-initialized` };
66
+ });
67
+
68
+ // ── 2. `bizar doctor` exits 0 ────────────────────────────────────────────
69
+ check('bizar doctor', () => {
70
+ try {
71
+ execSync('bizar doctor', {
72
+ stdio: ['ignore', 'pipe', 'pipe'],
73
+ timeout: timeoutMs,
74
+ encoding: 'utf8',
75
+ });
76
+ return { ok: true, message: 'bizar doctor passed' };
77
+ } catch (err) {
78
+ const status = err.status;
79
+ const msg = err.stderr || err.message;
80
+ if (status === 0) return { ok: true, message: 'bizar doctor passed' };
81
+ return { ok: false, message: `bizar doctor failed (exit ${status}): ${msg}`.slice(0, 200) };
82
+ }
83
+ });
84
+
85
+ // ── 3. Dashboard HTTP responds 200 ─────────────────────────────────────────
86
+ check('dashboard HTTP', async () => {
87
+ try {
88
+ const controller = new AbortController();
89
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
90
+ try {
91
+ const res = await fetch(`http://127.0.0.1:${port}/`, {
92
+ signal: controller.signal,
93
+ method: 'GET',
94
+ });
95
+ clearTimeout(timer);
96
+ if (res.ok) return { ok: true, message: `HTTP ${res.status} on port ${port}` };
97
+ return { ok: false, message: `HTTP ${res.status} on port ${port} (expected 200)` };
98
+ } catch (err) {
99
+ clearTimeout(timer);
100
+ if (err.name === 'AbortError') {
101
+ return { ok: false, message: `dashboard not responding on port ${port} (timeout)` };
102
+ }
103
+ return { ok: false, message: `dashboard unreachable on port ${port}: ${err.message}` };
104
+ }
105
+ } catch (err) {
106
+ return { ok: false, message: `fetch not available: ${err.message}` };
107
+ }
108
+ });
109
+
110
+ // ── 4. WebSocket connects ──────────────────────────────────────────────────
111
+ check('dashboard WebSocket', async () => {
112
+ try {
113
+ const { WebSocket } = await import('ws' in globalThis
114
+ ? { ws: globalThis.ws, WebSocket: globalThis.WebSocket }
115
+ : await import(`ws`).then(m => ({ ws: m, WebSocket: m.WebSocket || m.default }))
116
+ );
117
+ const url = `ws://127.0.0.1:${port}/ws`;
118
+ const controller = new AbortController();
119
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
120
+ try {
121
+ const ws = new WebSocket(url);
122
+ await new Promise((resolve, reject) => {
123
+ ws.on('open', resolve);
124
+ ws.on('error', reject);
125
+ ws.on('close', () => reject(new Error('closed before open')));
126
+ controller.signal.addEventListener('abort', () => {
127
+ ws.close();
128
+ reject(new Error('timeout'));
129
+ });
130
+ });
131
+ clearTimeout(timer);
132
+ ws.close();
133
+ return { ok: true, message: `WebSocket connected at ${url}` };
134
+ } catch (err) {
135
+ clearTimeout(timer);
136
+ if (err.message === 'timeout') {
137
+ return { ok: false, message: `WebSocket timeout on ${url}` };
138
+ }
139
+ return { ok: false, message: `WebSocket failed on ${url}: ${err.message}` };
140
+ }
141
+ } catch (err) {
142
+ // ws module not available in this environment — skip
143
+ if (err.code === 'MODULE_NOT_FOUND' || err.message?.includes('ws')) {
144
+ return { ok: true, message: 'ws module not available — skipping WebSocket check' };
145
+ }
146
+ return { ok: false, message: `WebSocket check failed: ${err.message}` };
147
+ }
148
+ });
149
+
150
+ // ── 5. `bizar bg list` returns ────────────────────────────────────────────
151
+ check('background agents', () => {
152
+ try {
153
+ execSync('bizar bg list', {
154
+ stdio: ['ignore', 'pipe', 'pipe'],
155
+ timeout: timeoutMs,
156
+ encoding: 'utf8',
157
+ });
158
+ return { ok: true, message: 'bizar bg list succeeded' };
159
+ } catch (err) {
160
+ return { ok: false, message: `bizar bg list failed: ${(err.stderr || err.message || '').slice(0, 200)}` };
161
+ }
162
+ });
163
+
164
+ // ── 6. lightrag-server --version ──────────────────────────────────────────
165
+ check('lightrag-server', () => {
166
+ const candidates = ['lightrag-server'];
167
+ if (process.env.HOME) {
168
+ candidates.push(join(process.env.HOME, '.local', 'bin', 'lightrag-server'));
169
+ }
170
+ let found = false;
171
+ for (const cmd of candidates) {
172
+ try {
173
+ const out = execFileSync(cmd, ['--version'], {
174
+ encoding: 'utf8',
175
+ timeout: 5000,
176
+ stdio: ['ignore', 'pipe', 'ignore'],
177
+ });
178
+ found = true;
179
+ return { ok: true, message: `${cmd} responds: ${(out || '').trim().slice(0, 100)}` };
180
+ } catch { /* try next */ }
181
+ }
182
+ return { ok: false, message: 'lightrag-server not found in PATH or ~/.local/bin/' };
183
+ });
184
+
185
+ // ── Summary ───────────────────────────────────────────────────────────────
186
+ const passed = checks.filter(c => c.ok).length;
187
+ const failed = checks.filter(c => !c.ok).length;
188
+ const allOk = failed === 0;
189
+
190
+ for (const c of checks) {
191
+ const marker = c.ok ? '✓' : '✗';
192
+ const msg = c.message ? ` ${marker} ${c.name}: ${c.message}` : ` ${marker} ${c.name}`;
193
+ if (c.ok) {
194
+ console.log(`\x1b[32m${msg}\x1b[0m`);
195
+ } else {
196
+ console.log(`\x1b[31m${msg}\x1b[0m`);
197
+ }
198
+ }
199
+
200
+ console.log(`\nSmoke test: ${passed} passed, ${failed} failed`);
201
+ return { ok: allOk, checks, passed, failed };
202
+ }
203
+
204
+ // ── CLI entry ─────────────────────────────────────────────────────────────────
205
+
206
+ const isMain = Boolean(process.argv[1]) && process.argv[1].endsWith('post-install-smoke.mjs');
207
+ if (isMain) {
208
+ const args = process.argv.slice(2);
209
+ const dryRun = args.includes('--dry-run');
210
+ const json = args.includes('--json');
211
+
212
+ if (dryRun) {
213
+ console.log('[dry-run] would run smoke test');
214
+ process.exit(0);
215
+ }
216
+
217
+ runSmokeTest().then(result => {
218
+ if (json) {
219
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
220
+ }
221
+ process.exit(result.ok ? 0 : 1);
222
+ }).catch(err => {
223
+ if (json) {
224
+ console.error(JSON.stringify({ error: err.message }));
225
+ } else {
226
+ console.error(`Smoke test error: ${err.message}`);
227
+ }
228
+ process.exit(1);
229
+ });
230
+ }