@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
package/cli/provision.mjs CHANGED
@@ -64,6 +64,7 @@ export const OPENCODE_DIR =
64
64
  const SERVICE_PID_FILE = join(BIZAR_HOME, 'service.pid');
65
65
  const DASHBOARD_PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
66
66
  const DASHBOARD_PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
67
+ const INSTALL_MARKER_FILE = join(BIZAR_HOME, 'installed.json');
67
68
 
68
69
  // ─── Tiny utilities ──────────────────────────────────────────────────────────
69
70
 
@@ -157,6 +158,35 @@ export async function killAndWait(pid, { timeoutMs = 5000, label = 'process' } =
157
158
  }
158
159
  }
159
160
 
161
+ // ─── Install marker ──────────────────────────────────────────────────────────
162
+
163
+ /**
164
+ * Read the install marker file. Returns null if missing.
165
+ * Marker shape: { version, installedAt, repoPath, serviceUnit }
166
+ */
167
+ export function readInstallMarker() {
168
+ return readJsonSafe(INSTALL_MARKER_FILE, null);
169
+ }
170
+
171
+ /**
172
+ * Write the install marker file.
173
+ */
174
+ export function writeInstallMarker({ version, repoPath, serviceUnit }) {
175
+ const marker = {
176
+ version: version || currentVersion(PKG_MAIN) || 'unknown',
177
+ installedAt: new Date().toISOString(),
178
+ repoPath: repoPath || REPO_ROOT,
179
+ serviceUnit: serviceUnit || null,
180
+ };
181
+ try {
182
+ mkdirSync(BIZAR_HOME, { recursive: true });
183
+ writeFileSync(INSTALL_MARKER_FILE, JSON.stringify(marker, null, 2) + '\n');
184
+ return { ok: true, marker };
185
+ } catch (err) {
186
+ return { ok: false, error: err.message };
187
+ }
188
+ }
189
+
160
190
  // ─── State detection ──────────────────────────────────────────────────────────
161
191
 
162
192
  /**
@@ -726,6 +756,55 @@ export async function runDoctor({ silent = false } = {}) {
726
756
  }
727
757
  }
728
758
 
759
+ /**
760
+ * Run the post-install smoke test.
761
+ */
762
+ export async function runSmokeTest({ silent = false } = {}) {
763
+ try {
764
+ const { runSmokeTest: smokeFn } = await import('./post-install-smoke.mjs');
765
+ const result = await smokeFn();
766
+ return result;
767
+ } catch (err) {
768
+ return {
769
+ ok: false,
770
+ checks: [{ name: 'smoke-test', ok: false, message: err.message }],
771
+ passed: 0,
772
+ failed: 1,
773
+ };
774
+ }
775
+ }
776
+
777
+ /**
778
+ * v5.x — Install LightRAG via uv tool.
779
+ * Called from the provisioner in case the shell script (install.sh)
780
+ * was skipped (e.g. Windows without WSL). Idempotent — safe to re-run.
781
+ * Fails gracefully if uv is not available.
782
+ */
783
+ export async function installLightragProvision({ dryRun = false } = {}) {
784
+ if (dryRun) {
785
+ return { ok: true, message: '[dry-run] would run: uv tool install "lightrag-hku[api]"' };
786
+ }
787
+ // Check if already installed
788
+ if (haveCmd('lightrag-server')) {
789
+ return { ok: true, message: 'lightrag-server already on PATH' };
790
+ }
791
+ if (!haveCmd('uv')) {
792
+ return { ok: false, message: 'uv not found — LightRAG not installed (install uv to enable)' };
793
+ }
794
+ try {
795
+ const r = spawnSync('uv', ['tool', 'install', 'lightrag-hku[api]'], {
796
+ stdio: 'inherit',
797
+ timeout: 120_000,
798
+ });
799
+ if (r.status === 0 || r.status === null) {
800
+ return { ok: true, message: 'lightrag-hku[api] installed via uv' };
801
+ }
802
+ return { ok: false, message: `uv tool install failed (exit ${r.status})` };
803
+ } catch (err) {
804
+ return { ok: false, message: `lightrag install error: ${err.message}` };
805
+ }
806
+ }
807
+
729
808
  /**
730
809
  * v4.4.11 — The mods step. By default, NEVER install or upgrade mods
731
810
  * during `bizar install` or `bizar update`. The step just reports the
@@ -960,6 +1039,28 @@ export async function runProvision(opts = {}) {
960
1039
  console.log(chalk.dim(' --dry-run set: no installs, kills, or restarts will be performed.\n'));
961
1040
  }
962
1041
 
1042
+ // ── 0.5. Idempotency: detect existing install ─────────────────────────
1043
+ // v5.x — If a marker file exists and we're in 'install' mode (not 'update'),
1044
+ // treat this as a re-install. In non-interactive mode, auto-upgrade.
1045
+ // In interactive mode, prompt the user.
1046
+ const marker = readInstallMarker();
1047
+ if (mode === 'install' && marker && !dryRun) {
1048
+ if (yes) {
1049
+ // Non-interactive: auto-switch to update mode
1050
+ console.log(chalk.yellow(' ⚠ Existing install detected (marker found).'));
1051
+ console.log(chalk.yellow(' Auto-switching to update mode due to --yes flag.'));
1052
+ console.log('');
1053
+ // Re-call runProvision with update mode
1054
+ return runProvision({ ...opts, mode: 'update' });
1055
+ } else {
1056
+ // Interactive: warn and continue
1057
+ console.log(chalk.yellow(' ⚠ Existing install detected (marker found at ~/.config/bizar/installed.json).'));
1058
+ console.log(chalk.yellow(` Last installed: ${marker.installedAt || 'unknown'}`));
1059
+ console.log(chalk.yellow(' To update an existing install, use `bizar update` or re-run with --update.'));
1060
+ console.log('');
1061
+ }
1062
+ }
1063
+
963
1064
  // ── 1. Detect state ──────────────────────────────────────────────────
964
1065
  const state = detectState();
965
1066
 
@@ -1036,6 +1137,9 @@ export async function runProvision(opts = {}) {
1036
1137
  await runStep('system-deps + service', () => ensureSystemDeps({ dryRun, mode }));
1037
1138
  }
1038
1139
 
1140
+ // ── 6.5. LightRAG install (Node-side, in case shell script skipped) ─
1141
+ await runStep('lightrag-server', () => installLightragProvision({ dryRun }));
1142
+
1039
1143
  // ── 7-9. Sync agent files, commands, skills, plugin, opencode.json ──
1040
1144
  console.log('');
1041
1145
  await runStep('agent files + slash commands + skills', () =>
@@ -1053,6 +1157,34 @@ export async function runProvision(opts = {}) {
1053
1157
  );
1054
1158
  await runStep('opencode.json plugin entry', () => patchOpencodeJson({ dryRun, force }));
1055
1159
 
1160
+ // ── 9.5. v5.x — issue #7. Restart the system service so it picks up
1161
+ // the freshly-installed binary. We do this AFTER the npm upgrade
1162
+ // (so the new code is on disk) and AFTER the file sync (so the
1163
+ // service won't try to read stale files on boot). The service has
1164
+ // already been killed in step 4 (mode==='update' branch).
1165
+ // This is also called on first install: if the service was registered
1166
+ // before the npm upgrade, restart is a no-op (unit content matches
1167
+ // and the service is already running with the old code).
1168
+ if (mode === 'update' && !dryRun) {
1169
+ try {
1170
+ const { restartService } = await import('./service-controller.mjs');
1171
+ const restart = restartService({ force: false, dryRun: false });
1172
+ if (restart.ok) {
1173
+ console.log(chalk.green(' ✓ Background service restarted with new code.'));
1174
+ stepResults.push({ label: 'service-restart', ok: true, message: 'restarted' });
1175
+ } else {
1176
+ // Non-fatal: the service may have been killed but failed to
1177
+ // restart, or it may not have been installed. The OS will
1178
+ // attempt to start it on next login either way.
1179
+ console.log(chalk.yellow(` ⚠ Service restart: ${restart.error || 'unknown'}`));
1180
+ stepResults.push({ label: 'service-restart', ok: false, message: restart.error || 'restart failed' });
1181
+ }
1182
+ } catch (err) {
1183
+ console.log(chalk.yellow(` ⚠ Service restart skipped: ${err?.message || err}`));
1184
+ stepResults.push({ label: 'service-restart', ok: false, message: err?.message || 'skipped' });
1185
+ }
1186
+ }
1187
+
1056
1188
  // ── 10. (update) Restart dashboard ──────────────────────────────────
1057
1189
  if (mode === 'update' && restart && state.dashboard.running) {
1058
1190
  console.log('');
@@ -1072,6 +1204,18 @@ export async function runProvision(opts = {}) {
1072
1204
  const modsStep = await runModsStep({ mode, dryRun, force, withMods, state });
1073
1205
  stepResults.push({ label: 'mods', ...modsStep });
1074
1206
 
1207
+ // ── 11.5. Write install marker (v5.x idempotency) ──────────────────
1208
+ if (!dryRun) {
1209
+ const markerWrite = writeInstallMarker({
1210
+ version: state.pkgVersion || null,
1211
+ repoPath: REPO_ROOT,
1212
+ serviceUnit: state.service.unitPath,
1213
+ });
1214
+ if (markerWrite.ok) {
1215
+ console.log(chalk.dim(' ✓ install marker updated'));
1216
+ }
1217
+ }
1218
+
1075
1219
  // ── 12. Doctor health check ───────────────────────────────────────
1076
1220
  console.log('');
1077
1221
  const doctor = await runDoctor({ silent: true });
@@ -1087,6 +1231,12 @@ export async function runProvision(opts = {}) {
1087
1231
  console.log(chalk.green(' ✓ Doctor: all checks passed'));
1088
1232
  }
1089
1233
 
1234
+ // ── 12.5. Smoke test ──────────────────────────────────────────────
1235
+ console.log('');
1236
+ console.log(chalk.bold(' → Smoke test'));
1237
+ const smoke = await runSmokeTest({ silent: false });
1238
+ stepResults.push({ label: 'smoke-test', ok: smoke.ok, message: `${smoke.passed} passed, ${smoke.failed} failed` });
1239
+
1090
1240
  // ── 12. Summary ─────────────────────────────────────────────────────
1091
1241
  console.log('');
1092
1242
  console.log(' Summary:');
@@ -1102,6 +1252,33 @@ export async function runProvision(opts = {}) {
1102
1252
  console.log(chalk.green(`\n ✓ ${mode === 'update' ? 'Update' : 'Install'} complete\n`));
1103
1253
  }
1104
1254
 
1255
+ // ── 13. API key bootstrap warning ────────────────────────────────────
1256
+ // v5.x — After a successful install, check whether any API keys are
1257
+ // configured. If not, surface a prominent warning (but don't block —
1258
+ // many users configure keys later).
1259
+ if (mode === 'install' && !dryRun && !anyFail) {
1260
+ const envJsonPath = join(BIZAR_HOME, 'env.json');
1261
+ const marker = readInstallMarker();
1262
+ const hasApiKeys = Boolean(
1263
+ process.env.OPENAI_API_KEY ||
1264
+ process.env.ANTHROPIC_API_KEY ||
1265
+ process.env.MINIMAX_API_KEY ||
1266
+ (existsSync(envJsonPath) && (() => {
1267
+ try {
1268
+ const env = JSON.parse(readFileSync(envJsonPath, 'utf8'));
1269
+ return Boolean(env?.OPENAI_API_KEY || env?.ANTHROPIC_API_KEY || env?.MINIMAX_API_KEY);
1270
+ } catch { return false; }
1271
+ })())
1272
+ );
1273
+ if (!hasApiKeys) {
1274
+ console.log(chalk.yellow(' ⚠ No API keys configured.'));
1275
+ console.log(chalk.yellow(' Run `bizar connect` to add providers.'));
1276
+ const dashPort = process.env.BIZAR_DASHBOARD_PORT || '4097';
1277
+ console.log(chalk.yellow(` Or visit http://localhost:${dashPort}/connect after starting the dashboard.`));
1278
+ console.log('');
1279
+ }
1280
+ }
1281
+
1105
1282
  return {
1106
1283
  ok: !anyFail,
1107
1284
  mode,
@@ -0,0 +1,180 @@
1
+ /**
2
+ * cli/provision.test.mjs
3
+ *
4
+ * v5.x — Tests for the idempotency marker and installLightrag functions
5
+ * added in the "fully functional installer" gap-closure.
6
+ */
7
+ import { test, describe, beforeEach, afterEach, after } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import {
10
+ mkdtempSync,
11
+ mkdirSync,
12
+ writeFileSync,
13
+ rmSync,
14
+ existsSync,
15
+ readFileSync,
16
+ unlinkSync,
17
+ } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+
21
+ // Mock HOME for all tests
22
+ const ORIG_HOME = process.env.HOME;
23
+ const ORIG_XDG = process.env.XDG_CONFIG_HOME;
24
+
25
+ function freshHome() {
26
+ const home = mkdtempSync(join(tmpdir(), 'bizar-provision-'));
27
+ process.env.HOME = home;
28
+ delete process.env.XDG_CONFIG_HOME;
29
+ delete process.env.BIZAR_DASHBOARD_PORT;
30
+ delete process.env.OPENCODE_SERVER_PASSWORD;
31
+ delete process.env.BIZAR_MEMORY_VAULT;
32
+ return home;
33
+ }
34
+
35
+ function restoreHome() {
36
+ if (ORIG_HOME === undefined) delete process.env.HOME;
37
+ else process.env.HOME = ORIG_HOME;
38
+ if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
39
+ else process.env.XDG_CONFIG_HOME = ORIG_XDG;
40
+ }
41
+
42
+ after(() => {
43
+ restoreHome();
44
+ });
45
+
46
+ // ── Idempotency marker ────────────────────────────────────────────────────────
47
+
48
+ describe('install marker (installed.json)', () => {
49
+ let home;
50
+
51
+ beforeEach(() => {
52
+ home = freshHome();
53
+ process.env.HOME = home;
54
+ delete process.env.XDG_CONFIG_HOME;
55
+ delete process.env.BIZAR_HOME;
56
+ });
57
+
58
+ afterEach(() => {
59
+ restoreHome();
60
+ if (home && existsSync(home)) rmSync(home, { recursive: true, force: true });
61
+ });
62
+
63
+ test('readInstallMarker returns null when no marker file', async () => {
64
+ // HOME is fresh tmpdir — no marker should exist
65
+ const { readInstallMarker } = await import('./provision.mjs');
66
+ const marker = readInstallMarker();
67
+ assert.equal(marker, null);
68
+ });
69
+
70
+ test('writeInstallMarker returns ok=true and marker has correct shape', async () => {
71
+ // Note: ESM module caching means BIZAR_HOME was computed at first module load.
72
+ // This test verifies the function returns the correct shape when called.
73
+ const { writeInstallMarker } = await import('./provision.mjs');
74
+ const result = writeInstallMarker({ version: '5.0.0', repoPath: '/test/repo' });
75
+ assert.equal(result.ok, true);
76
+ assert.equal(result.marker.version, '5.0.0');
77
+ assert.equal(result.marker.repoPath, '/test/repo');
78
+ assert.ok(result.marker.installedAt);
79
+ });
80
+
81
+ test('readInstallMarker returns existing marker from BIZAR_HOME', async () => {
82
+ const { writeInstallMarker, readInstallMarker } = await import('./provision.mjs');
83
+ // Write first (this uses the fresh HOME's .config/bizar)
84
+ writeInstallMarker({ version: '1.2.3', repoPath: '/foo' });
85
+ // Read back
86
+ const marker = readInstallMarker();
87
+ assert.ok(marker !== null);
88
+ assert.equal(marker.version, '1.2.3');
89
+ assert.equal(marker.repoPath, '/foo');
90
+ });
91
+ });
92
+
93
+ // ── installLightragProvision ───────────────────────────────────────────────
94
+
95
+ describe('installLightragProvision()', () => {
96
+ test('dryRun returns ok without making changes', async () => {
97
+ const { installLightragProvision } = await import('./provision.mjs');
98
+ const result = await installLightragProvision({ dryRun: true });
99
+ assert.equal(result.ok, true);
100
+ assert.ok(result.message.includes('[dry-run]'));
101
+ });
102
+
103
+ test('returns failure when uv not available', async () => {
104
+ const { installLightragProvision } = await import('./provision.mjs');
105
+ // uv is likely available in the test env, but if it is, the test still passes
106
+ // (already installed). The key is that it doesn't throw.
107
+ const result = await installLightragProvision({ dryRun: false });
108
+ // Result is either ok=true (already installed) or ok=false (uv missing)
109
+ // Either way, no exception was thrown.
110
+ assert.equal(typeof result.ok, 'boolean');
111
+ assert.ok(typeof result.message === 'string');
112
+ });
113
+ });
114
+
115
+ // ── buildServiceEnvFile ──────────────────────────────────────────────────────
116
+
117
+ describe('buildServiceEnvFile()', () => {
118
+ let home;
119
+
120
+ beforeEach(() => {
121
+ home = freshHome();
122
+ process.env.HOME = home;
123
+ delete process.env.XDG_CONFIG_HOME;
124
+ delete process.env.BIZAR_HOME;
125
+ });
126
+
127
+ afterEach(() => {
128
+ restoreHome();
129
+ if (home && existsSync(home)) rmSync(home, { recursive: true, force: true });
130
+ });
131
+
132
+ test('includes all required env vars', async () => {
133
+ const { buildServiceEnvFile } = await import('./service-env.mjs');
134
+ const content = buildServiceEnvFile({ repoPath: '/repo' });
135
+ const required = [
136
+ 'BIZAR_HOME', 'BIZAR_REPO', 'PATH',
137
+ 'OPENCODE_SERVER_PASSWORD', 'BIZAR_DASHBOARD_PORT',
138
+ 'BIZAR_DASHBOARD_HOST', 'BIZAR_LOG_LEVEL',
139
+ 'BIZAR_HEADROOM_AUTOSTART', 'BIZAR_LIGHTRAG_AUTOSTART',
140
+ 'BIZAR_MEMORY_VAULT',
141
+ ];
142
+ for (const var_ of required) {
143
+ assert.ok(
144
+ content.includes(`${var_}=`),
145
+ `${var_} should be present in env file`,
146
+ );
147
+ }
148
+ });
149
+
150
+ test('generates OPENCODE_SERVER_PASSWORD when not set', async () => {
151
+ const { buildServiceEnvFile } = await import('./service-env.mjs');
152
+ const content = buildServiceEnvFile({ repoPath: '/repo' });
153
+ const lines = content.split('\n');
154
+ const pwdLine = lines.find(l => l.startsWith('OPENCODE_SERVER_PASSWORD='));
155
+ assert.ok(pwdLine, 'OPENCODE_SERVER_PASSWORD line should exist');
156
+ const pwd = pwdLine.split('=')[1];
157
+ assert.ok(pwd.length > 0, 'password should be non-empty');
158
+ });
159
+
160
+ test('adds ~/.local/bin to PATH when uv tools are present', async () => {
161
+ const { buildServiceEnvFile } = await import('./service-env.mjs');
162
+ // Ensure HOME is set for the test
163
+ process.env.HOME = home;
164
+ const content = buildServiceEnvFile({ repoPath: '/repo' });
165
+ assert.ok(content.includes('.local/bin:'), 'PATH should include ~/.local/bin');
166
+ });
167
+
168
+ test('uses BIZAR_MEMORY_VAULT from env when set', async () => {
169
+ const { buildServiceEnvFile } = await import('./service-env.mjs');
170
+ process.env.BIZAR_MEMORY_VAULT = '/my/custom/vault';
171
+ try {
172
+ const content = buildServiceEnvFile({ repoPath: '/repo' });
173
+ assert.ok(content.includes('BIZAR_MEMORY_VAULT=/my/custom/vault'));
174
+ } finally {
175
+ delete process.env.BIZAR_MEMORY_VAULT;
176
+ }
177
+ });
178
+ });
179
+
180
+ console.log(' provision.mjs tests loaded — run with: node --test cli/provision.test.mjs');