@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.
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
- package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
- package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
- package/bizar-dash/dist/index.html +8 -8
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/backup-store.mjs +6 -1
- package/bizar-dash/src/server/bg-spawner.mjs +574 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/routes/background.mjs +143 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
- package/bizar-dash/src/server/routes/memory.mjs +8 -0
- package/bizar-dash/src/server/server.mjs +22 -0
- package/bizar-dash/src/web/App.tsx +3 -0
- package/bizar-dash/src/web/MobileApp.tsx +1 -10
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +43 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
- package/bizar-dash/src/web/styles/settings.css +46 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Settings.tsx +12 -12
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
- package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
- package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer.test.mjs +42 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
- package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
- package/bizar-dash/tests/memory-cli.test.mjs +1 -1
- package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
- package/bizar-dash/tests/settings-layout.test.tsx +3 -3
- package/bizar-dash/tests/settings-nav.test.tsx +7 -5
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/util.mjs +8 -1
- package/cli/init.mjs +1 -1
- package/cli/post-install-smoke.mjs +230 -0
- package/cli/provision.mjs +177 -0
- package/cli/provision.test.mjs +180 -0
- package/cli/service-controller.mjs +378 -28
- package/cli/service-controller.test.mjs +100 -0
- package/cli/service-env.mjs +139 -0
- package/cli/service.mjs +23 -0
- package/config/agents/_shared/AGENT_BASELINE.md +6 -6
- package/install.sh +87 -6
- package/package.json +1 -1
- package/plugins/bizar/index.ts +147 -0
- package/plugins/bizar/src/background-state.ts +84 -5
- package/plugins/bizar/src/background.ts +388 -10
- package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
- package/plugins/bizar/src/opencode-runner.ts +87 -1
- package/plugins/bizar/src/tools/bg-pause.ts +77 -0
- package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
- package/plugins/bizar/src/tools/bg-resume.ts +72 -0
- package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
- package/plugins/bizar/src/tools/bg-status.ts +10 -0
- package/plugins/bizar/src/tools/memory-list.ts +135 -0
- package/plugins/bizar/src/tools/memory-read.ts +142 -0
- package/plugins/bizar/src/tools/memory-search.ts +228 -0
- package/plugins/bizar/src/tools/memory-write.ts +183 -0
- package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
- package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
- package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
- package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
|
@@ -40,6 +40,7 @@ import { homedir, platform } from 'node:os';
|
|
|
40
40
|
import { join, dirname, resolve, isAbsolute } from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { bizarConfigDir } from './utils.mjs';
|
|
43
|
+
import { buildServiceEnvFile } from './service-env.mjs';
|
|
43
44
|
|
|
44
45
|
const __filename = fileURLToPath(import.meta.url);
|
|
45
46
|
const __dirname = dirname(__filename);
|
|
@@ -101,6 +102,9 @@ function linuxUnitPath() {
|
|
|
101
102
|
function linuxEnvPath() {
|
|
102
103
|
return join(bizarConfigDir(), 'service.env');
|
|
103
104
|
}
|
|
105
|
+
function linuxHeadroomUnitPath() {
|
|
106
|
+
return join(userUnitDir(), 'bizar-headroom.service');
|
|
107
|
+
}
|
|
104
108
|
function darwinPlistPath() {
|
|
105
109
|
return join(launchAgentsDir(), 'com.bizar.dashboard.plist');
|
|
106
110
|
}
|
|
@@ -140,7 +144,13 @@ function linuxUnitContent({ nodePath, projectRoot }) {
|
|
|
140
144
|
const cliEntry = join(projectRoot, 'cli', 'bin.mjs');
|
|
141
145
|
return [
|
|
142
146
|
'[Unit]',
|
|
143
|
-
'Description=Bizar background service daemon',
|
|
147
|
+
'Description=Bizar background service daemon (schedules + task-delegator)',
|
|
148
|
+
// v5.x — issue #7 — Make the service unit more robust.
|
|
149
|
+
// Bump StartLimitInterval so transient failures don't trip the
|
|
150
|
+
// systemd rate-limit (default 10s / 5 starts is too aggressive
|
|
151
|
+
// for a service that restarts after `bizar update`).
|
|
152
|
+
'StartLimitIntervalSec=300',
|
|
153
|
+
'StartLimitBurst=10',
|
|
144
154
|
'After=network-online.target',
|
|
145
155
|
'Wants=network-online.target',
|
|
146
156
|
'',
|
|
@@ -148,9 +158,15 @@ function linuxUnitContent({ nodePath, projectRoot }) {
|
|
|
148
158
|
'Type=simple',
|
|
149
159
|
`EnvironmentFile=${envPath}`,
|
|
150
160
|
`ExecStart=${nodePath} ${cliEntry} service _daemon`,
|
|
151
|
-
|
|
161
|
+
// Restart=on-failure means we only auto-restart on actual errors
|
|
162
|
+
// (non-zero exit). The default `always` would also restart on
|
|
163
|
+
// SIGTERM (which the user just used to stop us), making manual
|
|
164
|
+
// `systemctl --user stop` a fight against the supervisor.
|
|
165
|
+
'Restart=on-failure',
|
|
152
166
|
'RestartSec=5',
|
|
153
167
|
'TimeoutStopSec=20',
|
|
168
|
+
// Hard ceiling so a runaway daemon doesn't pin a CPU forever.
|
|
169
|
+
'CPUQuota=10%',
|
|
154
170
|
'',
|
|
155
171
|
'[Install]',
|
|
156
172
|
'WantedBy=default.target',
|
|
@@ -159,21 +175,163 @@ function linuxUnitContent({ nodePath, projectRoot }) {
|
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
function linuxEnvFileContent({ projectRoot }) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
return buildServiceEnvFile({ bizarHome: bizarConfigDir(), repoPath: projectRoot });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Headroom companion service (Linux) ─────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function linuxHeadroomUnitContent() {
|
|
184
|
+
const envPath = join(bizarConfigDir(), 'service.env');
|
|
185
|
+
return [
|
|
186
|
+
'[Unit]',
|
|
187
|
+
'Description=Bizar Headroom context-compression proxy',
|
|
188
|
+
'After=bizar.service',
|
|
189
|
+
'Wants=bizar.service',
|
|
190
|
+
'',
|
|
191
|
+
'[Service]',
|
|
192
|
+
'Type=simple',
|
|
193
|
+
`EnvironmentFile=${envPath}`,
|
|
194
|
+
'ExecStart=/bin/sh -c "command -v headroom >/dev/null 2>&1 && headroom proxy opencode || exit 0"',
|
|
195
|
+
'Restart=on-failure',
|
|
196
|
+
'RestartSec=10',
|
|
197
|
+
'TimeoutStopSec=15',
|
|
198
|
+
'',
|
|
199
|
+
'[Install]',
|
|
200
|
+
'WantedBy=default.target',
|
|
201
|
+
'',
|
|
202
|
+
].join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function darwinHeadroomPlistPath() {
|
|
206
|
+
return join(launchAgentsDir(), 'com.bizar.headroom.plist');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function darwinHeadroomPlistContent({ nodePath, projectRoot }) {
|
|
210
|
+
const envContent = buildServiceEnvFile({ bizarHome: bizarConfigDir(), repoPath: projectRoot });
|
|
211
|
+
const envVars = parseEnvFile(envContent);
|
|
212
|
+
const envXml = envVars
|
|
213
|
+
.map(([k, v]) => ` <key>${xmlEscape(k)}</key>\n <string>${xmlEscape(v)}</string>`)
|
|
214
|
+
.join('\n');
|
|
215
|
+
return [
|
|
216
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
217
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
218
|
+
'<plist version="1.0">',
|
|
219
|
+
'<dict>',
|
|
220
|
+
' <key>Label</key>',
|
|
221
|
+
' <string>com.bizar.headroom</string>',
|
|
222
|
+
' <key>ProgramArguments</key>',
|
|
223
|
+
' <array>',
|
|
224
|
+
` <string>${xmlEscape(nodePath)}</string>`,
|
|
225
|
+
' <string>-c</string>',
|
|
226
|
+
' <string>command -v headroom >/dev/null 2>&1 && headroom proxy opencode || exit 0</string>',
|
|
227
|
+
' </array>',
|
|
228
|
+
' <key>EnvironmentVariables</key>',
|
|
229
|
+
' <dict>',
|
|
230
|
+
envXml,
|
|
231
|
+
' </dict>',
|
|
232
|
+
' <key>KeepAlive</key>',
|
|
233
|
+
' <true/>',
|
|
234
|
+
' <key>RunAtLoad</key>',
|
|
235
|
+
' <true/>',
|
|
236
|
+
' <key>StandardOutPath</key>',
|
|
237
|
+
` <string>${xmlEscape(join(bizarConfigDir(), 'headroom.log'))}</string>`,
|
|
238
|
+
' <key>StandardErrorPath</key>',
|
|
239
|
+
` <string>${xmlEscape(join(bizarConfigDir(), 'headroom.log'))}</string>`,
|
|
240
|
+
' <key>WorkingDirectory</key>',
|
|
241
|
+
` <string>${xmlEscape(projectRoot)}</string>`,
|
|
242
|
+
'</dict>',
|
|
243
|
+
'</plist>',
|
|
244
|
+
'',
|
|
245
|
+
].join('\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function installHeadroomServiceLinux({ force = false }) {
|
|
249
|
+
// Honour BIZAR_HEADROOM_AUTOSTART from process.env (already merged by buildServiceEnvFile)
|
|
250
|
+
const headroomAutoStart = process.env.BIZAR_HEADROOM_AUTOSTART !== '0';
|
|
251
|
+
if (!headroomAutoStart) {
|
|
252
|
+
return ok({ note: 'headroom autostart disabled via BIZAR_HEADROOM_AUTOSTART=0' });
|
|
253
|
+
}
|
|
254
|
+
if (!existsSync(userUnitDir())) {
|
|
255
|
+
try { mkdirSync(userUnitDir(), { recursive: true }); } catch (err) {
|
|
256
|
+
return fail(`cannot create ${userUnitDir()}: ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const unitPath = linuxHeadroomUnitPath();
|
|
260
|
+
const want = linuxHeadroomUnitContent();
|
|
261
|
+
if (!force && existsSync(unitPath)) {
|
|
262
|
+
const have = readFileSync(unitPath, 'utf8').trim() + '\n';
|
|
263
|
+
if (have === want.trim() + '\n') {
|
|
264
|
+
return ok({ alreadyInstalled: true, unitPath, note: 'headroom unit already matches' });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
writeFileSync(unitPath, want, { encoding: 'utf8', mode: 0o644 });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return fail(`write ${unitPath}: ${err.message}`);
|
|
271
|
+
}
|
|
272
|
+
runCmd('systemctl', ['--user', 'daemon-reload']);
|
|
273
|
+
const r = runCmd('systemctl', ['--user', 'enable', '--now', 'bizar-headroom.service']);
|
|
274
|
+
if (r.error || (r.status !== 0 && r.status !== null)) {
|
|
275
|
+
return fail(`headroom service enable failed: ${r.stderr || r.error?.message || `exit ${r.status}`}`, { unitPath });
|
|
175
276
|
}
|
|
176
|
-
return
|
|
277
|
+
return ok({ unitPath, note: 'headroom companion service installed and started' });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function uninstallHeadroomServiceLinux() {
|
|
281
|
+
const unitPath = linuxHeadroomUnitPath();
|
|
282
|
+
if (!existsSync(unitPath)) {
|
|
283
|
+
return ok({ unitPath: null, note: 'no headroom systemd unit installed' });
|
|
284
|
+
}
|
|
285
|
+
runCmd('systemctl', ['--user', 'disable', '--now', 'bizar-headroom.service']);
|
|
286
|
+
try { unlinkSync(unitPath); } catch { /* ignore */ }
|
|
287
|
+
runCmd('systemctl', ['--user', 'daemon-reload']);
|
|
288
|
+
return ok({ unitPath: null, note: 'headroom systemd unit removed' });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function installHeadroomServiceDarwin({ nodePath, projectRoot, force = false }) {
|
|
292
|
+
const headroomAutoStart = process.env.BIZAR_HEADROOM_AUTOSTART !== '0';
|
|
293
|
+
if (!headroomAutoStart) {
|
|
294
|
+
return ok({ note: 'headroom autostart disabled via BIZAR_HEADROOM_AUTOSTART=0' });
|
|
295
|
+
}
|
|
296
|
+
const dir = launchAgentsDir();
|
|
297
|
+
if (!existsSync(dir)) {
|
|
298
|
+
try { mkdirSync(dir, { recursive: true }); } catch (err) {
|
|
299
|
+
return fail(`cannot create ${dir}: ${err.message}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const plistPath = darwinHeadroomPlistPath();
|
|
303
|
+
const want = darwinHeadroomPlistContent({ nodePath, projectRoot });
|
|
304
|
+
if (!force && existsSync(plistPath)) {
|
|
305
|
+
const have = readFileSync(plistPath, 'utf8').trim() + '\n';
|
|
306
|
+
if (have === want.trim() + '\n') {
|
|
307
|
+
const lst = runCmd('launchctl', ['list']);
|
|
308
|
+
if (lst.status === 0 && /com\.bizar\.headroom/.test(lst.stdout)) {
|
|
309
|
+
return ok({ alreadyInstalled: true, unitPath: plistPath, note: 'headroom plist matches and is loaded' });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
writeFileSync(plistPath, want, { encoding: 'utf8', mode: 0o644 });
|
|
315
|
+
try { chmodSync(plistPath, 0o644); } catch { /* best-effort */ }
|
|
316
|
+
} catch (err) {
|
|
317
|
+
return fail(`write ${plistPath}: ${err.message}`);
|
|
318
|
+
}
|
|
319
|
+
runCmd('launchctl', ['unload', plistPath]);
|
|
320
|
+
const load = runCmd('launchctl', ['load', '-w', plistPath]);
|
|
321
|
+
if (load.error || (load.status !== 0 && load.status !== null)) {
|
|
322
|
+
return fail(`launchctl load failed for headroom: ${load.stderr || load.error?.message || `exit ${load.status}`}`, { unitPath: plistPath });
|
|
323
|
+
}
|
|
324
|
+
return ok({ unitPath: plistPath, note: 'headroom companion plist installed and loaded' });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function uninstallHeadroomServiceDarwin() {
|
|
328
|
+
const plistPath = darwinHeadroomPlistPath();
|
|
329
|
+
if (!existsSync(plistPath)) {
|
|
330
|
+
return ok({ unitPath: null, note: 'no headroom launchd plist installed' });
|
|
331
|
+
}
|
|
332
|
+
runCmd('launchctl', ['unload', plistPath]);
|
|
333
|
+
try { unlinkSync(plistPath); } catch { /* ignore */ }
|
|
334
|
+
return ok({ unitPath: null, note: 'headroom launchd plist removed' });
|
|
177
335
|
}
|
|
178
336
|
|
|
179
337
|
function installServiceLinux({ nodePath, projectRoot, force = false }) {
|
|
@@ -235,6 +393,13 @@ function installServiceLinux({ nodePath, projectRoot, force = false }) {
|
|
|
235
393
|
return fail(`systemctl --user enable --now failed: ${r.stderr || r.error?.message || `exit ${r.status}`}`, { unitPath });
|
|
236
394
|
}
|
|
237
395
|
|
|
396
|
+
// Also install headroom companion service (non-fatal if it fails)
|
|
397
|
+
const hr = installHeadroomServiceLinux({ force });
|
|
398
|
+
if (!hr.ok) {
|
|
399
|
+
// Non-fatal: headroom is optional
|
|
400
|
+
console.log(` ${hr.error || 'headroom companion install failed'}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
238
403
|
return ok({
|
|
239
404
|
unitPath,
|
|
240
405
|
note: 'systemd user unit installed and started (or already running)',
|
|
@@ -261,6 +426,14 @@ function uninstallServiceLinux() {
|
|
|
261
426
|
});
|
|
262
427
|
}
|
|
263
428
|
|
|
429
|
+
function stopServiceLinux() {
|
|
430
|
+
const r = runCmd('systemctl', ['--user', 'stop', 'bizar.service']);
|
|
431
|
+
if (r.error || (r.status !== 0 && r.status !== null)) {
|
|
432
|
+
return { ok: false, error: `systemctl stop failed: ${r.stderr || r.error?.message || `exit ${r.status}`}` };
|
|
433
|
+
}
|
|
434
|
+
return { ok: true, note: 'bizar.service stopped' };
|
|
435
|
+
}
|
|
436
|
+
|
|
264
437
|
function linuxServiceStatus() {
|
|
265
438
|
const unitPath = linuxUnitPath();
|
|
266
439
|
if (!existsSync(unitPath)) {
|
|
@@ -277,11 +450,9 @@ function linuxServiceStatus() {
|
|
|
277
450
|
|
|
278
451
|
function darwinPlistContent({ nodePath, projectRoot }) {
|
|
279
452
|
const cliEntry = join(projectRoot, 'cli', 'bin.mjs');
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
['BIZAR_REPO', projectRoot],
|
|
284
|
-
];
|
|
453
|
+
// Use buildServiceEnvFile to get all env vars (honours existing process.env)
|
|
454
|
+
const envContent = buildServiceEnvFile({ bizarHome: bizarConfigDir(), repoPath: projectRoot });
|
|
455
|
+
const envVars = parseEnvFile(envContent);
|
|
285
456
|
const envXml = envVars
|
|
286
457
|
.map(([k, v]) => ` <key>${xmlEscape(k)}</key>\n <string>${xmlEscape(v)}</string>`)
|
|
287
458
|
.join('\n');
|
|
@@ -326,6 +497,25 @@ function xmlEscape(s) {
|
|
|
326
497
|
.replace(/>/g, '>');
|
|
327
498
|
}
|
|
328
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Parse a shell-style env file (KEY=VALUE lines, # comments) into
|
|
502
|
+
* an array of [key, value] tuples. Used by darwinPlistContent to
|
|
503
|
+
* embed all env vars in the launchd plist.
|
|
504
|
+
*/
|
|
505
|
+
function parseEnvFile(content) {
|
|
506
|
+
const result = [];
|
|
507
|
+
for (const line of content.split('\n')) {
|
|
508
|
+
const trimmed = line.trim();
|
|
509
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
510
|
+
const eqIdx = trimmed.indexOf('=');
|
|
511
|
+
if (eqIdx < 0) continue;
|
|
512
|
+
const key = trimmed.slice(0, eqIdx);
|
|
513
|
+
const value = trimmed.slice(eqIdx + 1);
|
|
514
|
+
result.push([key, value]);
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
|
|
329
519
|
function installServiceDarwin({ nodePath, projectRoot, force = false }) {
|
|
330
520
|
const dir = launchAgentsDir();
|
|
331
521
|
if (!existsSync(dir)) {
|
|
@@ -371,6 +561,13 @@ function installServiceDarwin({ nodePath, projectRoot, force = false }) {
|
|
|
371
561
|
if (load.error || (load.status !== 0 && load.status !== null)) {
|
|
372
562
|
return fail(`launchctl load failed: ${load.stderr || load.error?.message || `exit ${load.status}`}`, { unitPath: plistPath });
|
|
373
563
|
}
|
|
564
|
+
|
|
565
|
+
// Also install headroom companion service (non-fatal if it fails)
|
|
566
|
+
const hr = installHeadroomServiceDarwin({ nodePath, projectRoot, force });
|
|
567
|
+
if (!hr.ok) {
|
|
568
|
+
console.log(` ${hr.error || 'headroom companion install failed'}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
374
571
|
return ok({ unitPath: plistPath, note: 'plist installed and loaded' });
|
|
375
572
|
}
|
|
376
573
|
|
|
@@ -389,6 +586,15 @@ function uninstallServiceDarwin() {
|
|
|
389
586
|
return ok({ unitPath: null, note: 'plist removed' });
|
|
390
587
|
}
|
|
391
588
|
|
|
589
|
+
function stopServiceDarwin() {
|
|
590
|
+
const plistPath = darwinPlistPath();
|
|
591
|
+
const r = runCmd('launchctl', ['unload', plistPath]);
|
|
592
|
+
if (r.error || (r.status !== 0 && r.status !== null)) {
|
|
593
|
+
return { ok: false, error: `launchctl unload failed: ${r.stderr || r.error?.message || `exit ${r.status}`}` };
|
|
594
|
+
}
|
|
595
|
+
return { ok: true, note: 'com.bizar.dashboard stopped' };
|
|
596
|
+
}
|
|
597
|
+
|
|
392
598
|
function darwinServiceStatus() {
|
|
393
599
|
const plistPath = darwinPlistPath();
|
|
394
600
|
const lst = runCmd('launchctl', ['list']);
|
|
@@ -404,12 +610,22 @@ function darwinServiceStatus() {
|
|
|
404
610
|
|
|
405
611
|
function windowsCmdContent({ nodePath, projectRoot }) {
|
|
406
612
|
const cliEntry = join(projectRoot, 'cli', 'bin.mjs');
|
|
613
|
+
const envContent = buildServiceEnvFile({ bizarHome: bizarConfigDir(), repoPath: projectRoot });
|
|
614
|
+
const envLines = [];
|
|
615
|
+
for (const line of envContent.split('\n')) {
|
|
616
|
+
const trimmed = line.trim();
|
|
617
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
618
|
+
const eqIdx = trimmed.indexOf('=');
|
|
619
|
+
if (eqIdx < 0) continue;
|
|
620
|
+
const key = trimmed.slice(0, eqIdx);
|
|
621
|
+
const value = trimmed.slice(eqIdx + 1);
|
|
622
|
+
envLines.push(`set "${key}=${value.replace(/"/g, '\\"')}"`);
|
|
623
|
+
}
|
|
407
624
|
const lines = [
|
|
408
625
|
'@echo off',
|
|
409
626
|
'REM Generated by bizar service install — do not edit by hand.',
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
`:loop`,
|
|
627
|
+
...envLines,
|
|
628
|
+
':loop',
|
|
413
629
|
`node "${cliEntry}" service _daemon`,
|
|
414
630
|
'REM Restart with 5s backoff on exit. /b keeps this window alive.',
|
|
415
631
|
'timeout /t 5 /nobreak >nul',
|
|
@@ -481,6 +697,15 @@ function uninstallServiceWindows() {
|
|
|
481
697
|
});
|
|
482
698
|
}
|
|
483
699
|
|
|
700
|
+
function stopServiceWindows() {
|
|
701
|
+
const taskName = 'BizarDashboardService';
|
|
702
|
+
const r = runCmd('cmd.exe', ['/c', 'schtasks', '/End', '/TN', taskName]);
|
|
703
|
+
if (r.error || (r.status !== 0 && r.status !== null)) {
|
|
704
|
+
return { ok: false, error: `schtasks /End failed: ${r.stderr || r.error?.message || `exit ${r.status}`}` };
|
|
705
|
+
}
|
|
706
|
+
return { ok: true, note: 'BizarDashboardService stopped' };
|
|
707
|
+
}
|
|
708
|
+
|
|
484
709
|
function isScheduledTaskInstalled(taskName) {
|
|
485
710
|
// schtasks /Query is silent on stdout when the task is missing.
|
|
486
711
|
const r = runCmd('cmd.exe', ['/c', 'schtasks', '/Query', '/TN', taskName], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -525,10 +750,128 @@ export function installService(opts = {}) {
|
|
|
525
750
|
}
|
|
526
751
|
|
|
527
752
|
export function uninstallService() {
|
|
528
|
-
|
|
529
|
-
if (PLATFORM === '
|
|
530
|
-
if (PLATFORM === '
|
|
531
|
-
|
|
753
|
+
let result;
|
|
754
|
+
if (PLATFORM === 'linux') result = uninstallServiceLinux();
|
|
755
|
+
else if (PLATFORM === 'darwin') result = uninstallServiceDarwin();
|
|
756
|
+
else if (PLATFORM === 'win32') result = uninstallServiceWindows();
|
|
757
|
+
else return fail(`unsupported platform: ${PLATFORM}`);
|
|
758
|
+
// Also uninstall headroom companion
|
|
759
|
+
if (PLATFORM === 'linux') uninstallHeadroomServiceLinux();
|
|
760
|
+
if (PLATFORM === 'darwin') uninstallHeadroomServiceDarwin();
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Install the headroom companion service only (does NOT install the
|
|
766
|
+
* main dashboard service). Idempotent. Safe to re-run.
|
|
767
|
+
*/
|
|
768
|
+
export function installHeadroomService(opts = {}) {
|
|
769
|
+
const nodePath = resolveNodePath(opts.nodePath);
|
|
770
|
+
const projectRoot = resolveRepoRoot(opts.projectRoot);
|
|
771
|
+
if (PLATFORM === 'linux') return installHeadroomServiceLinux({ force: !!opts.force });
|
|
772
|
+
if (PLATFORM === 'darwin') return installHeadroomServiceDarwin({ nodePath, projectRoot, force: !!opts.force });
|
|
773
|
+
return ok({ note: `headroom companion not supported on ${PLATFORM}` });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function uninstallHeadroomService() {
|
|
777
|
+
if (PLATFORM === 'linux') return uninstallHeadroomServiceLinux();
|
|
778
|
+
if (PLATFORM === 'darwin') return uninstallHeadroomServiceDarwin();
|
|
779
|
+
return ok({ note: `headroom companion not supported on ${PLATFORM}` });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* v5.x — Restart the service (issue #7).
|
|
784
|
+
*
|
|
785
|
+
* The update flow replaces the on-disk files that the running service is
|
|
786
|
+
* holding open. Without a stop/start cycle, the service either crashes
|
|
787
|
+
* when the underlying inode is replaced (Linux) or runs forever against a
|
|
788
|
+
* stale binary (macOS / Windows).
|
|
789
|
+
*
|
|
790
|
+
* This function:
|
|
791
|
+
* 1. Stops the running service (systemctl stop / launchctl unload /
|
|
792
|
+
* schtasks /End). Best-effort — if the service isn't running, we
|
|
793
|
+
* treat that as success and continue.
|
|
794
|
+
* 2. Waits up to `waitMs` (default 10s) for the service to exit.
|
|
795
|
+
* 3. Re-installs the unit file. Idempotent — if the content hasn't
|
|
796
|
+
* changed, installService returns alreadyInstalled=true.
|
|
797
|
+
* 4. Starts the service again (systemctl --user enable --now / launchctl
|
|
798
|
+
* load -w / schtasks /Run).
|
|
799
|
+
*
|
|
800
|
+
* Returns a structured { ok, stopped, installed, started, error? }.
|
|
801
|
+
*
|
|
802
|
+
* @param {{ nodePath?: string, projectRoot?: string, force?: boolean, waitMs?: number, timeoutMs?: number, dryRun?: boolean }} [opts]
|
|
803
|
+
*/
|
|
804
|
+
export function restartService(opts = {}) {
|
|
805
|
+
const { waitMs = 10_000, dryRun = false } = opts;
|
|
806
|
+
const nodePath = resolveNodePath(opts.nodePath);
|
|
807
|
+
const projectRoot = resolveRepoRoot(opts.projectRoot);
|
|
808
|
+
const force = !!opts.force;
|
|
809
|
+
|
|
810
|
+
if (dryRun) {
|
|
811
|
+
return ok({
|
|
812
|
+
stopped: true,
|
|
813
|
+
installed: { alreadyInstalled: true },
|
|
814
|
+
started: true,
|
|
815
|
+
note: 'dry-run: would stop, reinstall, and start the service',
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// 1. Stop (best-effort)
|
|
820
|
+
let stopped;
|
|
821
|
+
try {
|
|
822
|
+
if (PLATFORM === 'linux') stopped = stopServiceLinux();
|
|
823
|
+
else if (PLATFORM === 'darwin') stopped = stopServiceDarwin();
|
|
824
|
+
else if (PLATFORM === 'win32') stopped = stopServiceWindows();
|
|
825
|
+
else return fail(`unsupported platform: ${PLATFORM}`);
|
|
826
|
+
} catch (err) {
|
|
827
|
+
return fail(`stop failed: ${err.message}`, { stopped: { ok: false, error: err.message } });
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// 2. Wait for the service to actually exit. The stop shell-outs are
|
|
831
|
+
// synchronous in the per-platform functions (systemctl blocks until
|
|
832
|
+
// stop completes; launchctl unload returns immediately; schtasks
|
|
833
|
+
// /End returns immediately), so we additionally check the running
|
|
834
|
+
// state and wait until the OS reports not-running OR our deadline
|
|
835
|
+
// elapses.
|
|
836
|
+
const deadline = Date.now() + waitMs;
|
|
837
|
+
if (PLATFORM === 'linux' || PLATFORM === 'darwin' || PLATFORM === 'win32') {
|
|
838
|
+
while (Date.now() < deadline) {
|
|
839
|
+
const st = serviceStatus();
|
|
840
|
+
if (!st.running) break;
|
|
841
|
+
// Synchronous short sleep — service should exit in <2s on a
|
|
842
|
+
// healthy system. We avoid pulling in a timer just for this
|
|
843
|
+
// bounded wait.
|
|
844
|
+
const until = Date.now() + 200;
|
|
845
|
+
while (Date.now() < until) { /* spin briefly */ }
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// 3. Reinstall
|
|
850
|
+
let installed;
|
|
851
|
+
try {
|
|
852
|
+
installed = installService({ nodePath, projectRoot, force });
|
|
853
|
+
} catch (err) {
|
|
854
|
+
return fail(`reinstall failed: ${err.message}`, { stopped, installed: { ok: false, error: err.message } });
|
|
855
|
+
}
|
|
856
|
+
if (!installed.ok) {
|
|
857
|
+
return fail(`reinstall failed: ${installed.error}`, { stopped, installed });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// 4. Start. The installService* helpers above already do an
|
|
861
|
+
// `enable --now` / `load -w` / `schtasks /Create` (the Windows
|
|
862
|
+
// wrapper script contains a self-restart loop, so creating the
|
|
863
|
+
// task starts the daemon). So when installService succeeds, the
|
|
864
|
+
// service is already running on every platform. Surface that as
|
|
865
|
+
// `started: true` here. The Status field of the return value is
|
|
866
|
+
// still authoritative — operators can call `bizar service status`
|
|
867
|
+
// to confirm.
|
|
868
|
+
return ok({
|
|
869
|
+
stopped,
|
|
870
|
+
installed,
|
|
871
|
+
started: true,
|
|
872
|
+
unitPath: installed.unitPath || serviceUnitPath({ projectRoot }),
|
|
873
|
+
note: 'service stopped, unit reinstalled, service started',
|
|
874
|
+
});
|
|
532
875
|
}
|
|
533
876
|
|
|
534
877
|
export function isInstalled() {
|
|
@@ -559,6 +902,12 @@ if (isMain) {
|
|
|
559
902
|
console.log(fmt(installService({ force, dryRun: dry })));
|
|
560
903
|
} else if (sub === 'uninstall') {
|
|
561
904
|
console.log(fmt(uninstallService()));
|
|
905
|
+
} else if (sub === 'restart') {
|
|
906
|
+
// v5.x — issue #7. Stop → reinstall unit → start. Idempotent on the
|
|
907
|
+
// install step (when the unit content hasn't changed, the second
|
|
908
|
+
// installService call returns alreadyInstalled=true and the service
|
|
909
|
+
// is still restarted).
|
|
910
|
+
console.log(fmt(restartService({ force, dryRun: dry })));
|
|
562
911
|
} else if (sub === 'status') {
|
|
563
912
|
console.log(fmt(serviceStatus()));
|
|
564
913
|
} else if (sub === 'is-installed') {
|
|
@@ -570,6 +919,7 @@ service-controller — platform-aware installer for the Bizar background service
|
|
|
570
919
|
Usage:
|
|
571
920
|
node cli/service-controller.mjs install [--force] [--dry-run]
|
|
572
921
|
node cli/service-controller.mjs uninstall
|
|
922
|
+
node cli/service-controller.mjs restart [--force] [--dry-run]
|
|
573
923
|
node cli/service-controller.mjs status
|
|
574
924
|
node cli/service-controller.mjs is-installed
|
|
575
925
|
`);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* cli/service-controller.test.mjs
|
|
3
3
|
*
|
|
4
4
|
* v4.4.0 — Smoke tests for the platform-aware service installer.
|
|
5
|
+
* v5.x — Adds tests for the `restartService()` lifecycle (issue #7).
|
|
5
6
|
*
|
|
6
7
|
* Strategy: focus on the parts that are platform-portable and don't
|
|
7
8
|
* require touching the user's real `$XDG_CONFIG_HOME/systemd/`:
|
|
@@ -10,6 +11,8 @@
|
|
|
10
11
|
* - `installService({ dryRun: true })` is side-effect free
|
|
11
12
|
* - `installService()` is idempotent (run twice → second reports
|
|
12
13
|
* `alreadyInstalled: true`)
|
|
14
|
+
* - `restartService({ dryRun: true })` is side-effect free
|
|
15
|
+
* - `restartService({ dryRun: true })` returns the documented shape
|
|
13
16
|
*
|
|
14
17
|
* Tests that actually install / uninstall on the host machine are
|
|
15
18
|
* skipped by default — opt in by setting `BIZAR_TEST_INSTALL=1`.
|
|
@@ -23,6 +26,7 @@ import { join } from 'node:path';
|
|
|
23
26
|
import {
|
|
24
27
|
installService,
|
|
25
28
|
uninstallService,
|
|
29
|
+
restartService,
|
|
26
30
|
serviceStatus,
|
|
27
31
|
serviceUnitPath,
|
|
28
32
|
} from './service-controller.mjs';
|
|
@@ -90,3 +94,99 @@ test('file permissions on the env file are 0600 (Linux/macOS only)', { skip: HOS
|
|
|
90
94
|
}
|
|
91
95
|
uninstallService();
|
|
92
96
|
});
|
|
97
|
+
|
|
98
|
+
// ── v5.x — restartService lifecycle (issue #7) ──────────────────────────────
|
|
99
|
+
|
|
100
|
+
test('restartService({ dryRun: true }) makes no filesystem changes', () => {
|
|
101
|
+
const before = serviceStatus();
|
|
102
|
+
const r = restartService({ dryRun: true });
|
|
103
|
+
assert.equal(r.ok, true, `dry-run restart returned ok=false: ${r.error}`);
|
|
104
|
+
const after = serviceStatus();
|
|
105
|
+
// dry-run must not flip the installed bit.
|
|
106
|
+
assert.equal(before.installed, after.installed);
|
|
107
|
+
assert.equal(before.running, after.running);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('restartService({ dryRun: true }) returns the documented shape', () => {
|
|
111
|
+
const r = restartService({ dryRun: true });
|
|
112
|
+
assert.equal(r.ok, true);
|
|
113
|
+
assert.equal(r.started, true, 'dry-run reports started=true');
|
|
114
|
+
// stopped/installed are present (even if their inner shape is
|
|
115
|
+
// minimal in dry-run mode).
|
|
116
|
+
assert.ok('stopped' in r, 'stopped field present');
|
|
117
|
+
assert.ok('installed' in r, 'installed field present');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('restartService() on a fresh install (host-only)', { skip: !ALLOW_INSTALL }, () => {
|
|
121
|
+
// Tear down, install, then restart and confirm the unit path is
|
|
122
|
+
// reported. We don't assert that the service is running because
|
|
123
|
+
// the OS init system (systemd / launchd / schtasks) may not be
|
|
124
|
+
// active in the test environment.
|
|
125
|
+
uninstallService();
|
|
126
|
+
const r1 = installService({});
|
|
127
|
+
assert.equal(r1.ok, true, `install failed: ${r1.error}`);
|
|
128
|
+
|
|
129
|
+
const r2 = restartService({});
|
|
130
|
+
assert.equal(r2.ok, true, `restart failed: ${r2.error}`);
|
|
131
|
+
assert.ok(typeof r2.unitPath === 'string', 'unitPath reported');
|
|
132
|
+
assert.equal(r2.started, true, 'restart reports started=true');
|
|
133
|
+
|
|
134
|
+
// The service should still be reported as installed.
|
|
135
|
+
const status = serviceStatus();
|
|
136
|
+
assert.equal(status.installed, true, 'service still installed after restart');
|
|
137
|
+
|
|
138
|
+
uninstallService();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── v5.x — buildServiceEnvFile + parseEnvFile ────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test('buildServiceEnvFile returns string with all required vars', async () => {
|
|
144
|
+
const { buildServiceEnvFile } = await import('./service-env.mjs');
|
|
145
|
+
const content = buildServiceEnvFile({ repoPath: '/test/repo' });
|
|
146
|
+
assert.ok(typeof content === 'string');
|
|
147
|
+
assert.ok(content.includes('BIZAR_HOME='), 'BIZAR_HOME present');
|
|
148
|
+
assert.ok(content.includes('BIZAR_REPO=/test/repo'), 'BIZAR_REPO present');
|
|
149
|
+
assert.ok(content.includes('PATH='), 'PATH present');
|
|
150
|
+
assert.ok(content.includes('OPENCODE_SERVER_PASSWORD='), 'OPENCODE_SERVER_PASSWORD present');
|
|
151
|
+
assert.ok(content.includes('BIZAR_DASHBOARD_PORT='), 'BIZAR_DASHBOARD_PORT present');
|
|
152
|
+
assert.ok(content.includes('BIZAR_DASHBOARD_HOST='), 'BIZAR_DASHBOARD_HOST present');
|
|
153
|
+
assert.ok(content.includes('BIZAR_LOG_LEVEL='), 'BIZAR_LOG_LEVEL present');
|
|
154
|
+
assert.ok(content.includes('BIZAR_HEADROOM_AUTOSTART='), 'BIZAR_HEADROOM_AUTOSTART present');
|
|
155
|
+
assert.ok(content.includes('BIZAR_LIGHTRAG_AUTOSTART='), 'BIZAR_LIGHTRAG_AUTOSTART present');
|
|
156
|
+
assert.ok(content.includes('BIZAR_MEMORY_VAULT='), 'BIZAR_MEMORY_VAULT present');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('buildServiceEnvFile honours existing process.env values', async () => {
|
|
160
|
+
const { buildServiceEnvFile } = await import('./service-env.mjs');
|
|
161
|
+
const origPort = process.env.BIZAR_DASHBOARD_PORT;
|
|
162
|
+
process.env.BIZAR_DASHBOARD_PORT = '9999';
|
|
163
|
+
try {
|
|
164
|
+
const content = buildServiceEnvFile({ repoPath: '/test/repo' });
|
|
165
|
+
assert.ok(content.includes('BIZAR_DASHBOARD_PORT=9999'), 'honoured BIZAR_DASHBOARD_PORT from env');
|
|
166
|
+
} finally {
|
|
167
|
+
if (origPort !== undefined) process.env.BIZAR_DASHBOARD_PORT = origPort;
|
|
168
|
+
else delete process.env.BIZAR_DASHBOARD_PORT;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('buildServiceEnvFile uses default BIZAR_MEMORY_VAULT when not set', async () => {
|
|
173
|
+
const { buildServiceEnvFile } = await import('./service-env.mjs');
|
|
174
|
+
const orig = process.env.BIZAR_MEMORY_VAULT;
|
|
175
|
+
delete process.env.BIZAR_MEMORY_VAULT;
|
|
176
|
+
try {
|
|
177
|
+
const content = buildServiceEnvFile({ repoPath: '/test/repo' });
|
|
178
|
+
assert.ok(content.includes('BIZAR_MEMORY_VAULT='), 'BIZAR_MEMORY_VAULT present');
|
|
179
|
+
assert.ok(content.includes('.bizar_memory'), 'default vault path present');
|
|
180
|
+
} finally {
|
|
181
|
+
if (orig !== undefined) process.env.BIZAR_MEMORY_VAULT = orig;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('darwinPlistContent generates XML with BIZAR env vars (dry-run only — no file written)', async () => {
|
|
186
|
+
// dry-run test: verify darwinPlistContent is accessible and produces XML.
|
|
187
|
+
// We test via installService({ dryRun: true }) which uses darwinPlistContent.
|
|
188
|
+
const r = installService({ dryRun: true });
|
|
189
|
+
assert.equal(r.ok, true);
|
|
190
|
+
assert.ok(typeof r.note === 'string');
|
|
191
|
+
});
|
|
192
|
+
|