@polderlabs/bizar 3.7.2 → 3.8.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/cli/bin.mjs +60 -11
- package/cli/graph.mjs +337 -0
- package/cli/graph.test.mjs +218 -0
- package/cli/init.mjs +35 -0
- package/cli/install.mjs +58 -0
- package/cli/update.mjs +371 -74
- package/config/AGENTS.md +53 -0
- package/config/agents/baldr.md +24 -0
- package/config/agents/forseti.md +9 -0
- package/config/agents/heimdall.md +24 -0
- package/config/agents/hermod.md +20 -0
- package/config/agents/mimir.md +24 -0
- package/config/agents/odin.md +45 -0
- package/config/agents/thor.md +24 -0
- package/config/agents/tyr.md +24 -0
- package/config/agents/vidarr.md +24 -0
- package/config/commands/init.md +22 -1
- package/package.json +1 -1
package/cli/update.mjs
CHANGED
|
@@ -1,36 +1,180 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* update.mjs — `bizar update` subcommand.
|
|
3
3
|
*
|
|
4
|
-
* Updates the opencode CLI, the @polderlabs/bizar package,
|
|
5
|
-
* @polderlabs/bizar-
|
|
6
|
-
* component individually. With
|
|
7
|
-
*
|
|
8
|
-
* runs only the named update.
|
|
4
|
+
* Updates the opencode CLI, the @polderlabs/bizar package, the
|
|
5
|
+
* @polderlabs/bizar-dash package, and/or the @polderlabs/bizar-plugin
|
|
6
|
+
* package. By default, prompts for each component individually. With
|
|
7
|
+
* `--all`, updates everything without prompting.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Before any install, the command:
|
|
10
|
+
* 1. Detects running instances (background service daemon, dashboard
|
|
11
|
+
* server, TUI dashboard) by reading the PID files at
|
|
12
|
+
* ~/.config/bizar/{service,dashboard}.pid and cleaning up any
|
|
13
|
+
* stale or empty ones.
|
|
14
|
+
* 2. Warns the user explicitly about each running instance, lists
|
|
15
|
+
* what will be killed, and asks for confirmation. Skipped only
|
|
16
|
+
* with `--yes` (or `--force`).
|
|
17
|
+
* 3. Sends SIGTERM to each live instance, waits up to 5s for
|
|
18
|
+
* graceful shutdown, then escalates to SIGKILL for anything
|
|
19
|
+
* still alive.
|
|
14
20
|
*
|
|
15
|
-
* After
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* After a successful update of `bizar` or `bizar-dash`, the dashboard
|
|
22
|
+
* is automatically restarted (using its own `POST /api/restart`
|
|
23
|
+
* endpoint when reachable, or by spawning a new process when not).
|
|
24
|
+
* Use `--no-restart` to skip the restart.
|
|
25
|
+
*
|
|
26
|
+
* After the npm packages are updated, the install script is re-run so
|
|
27
|
+
* the locally deployed plugin source matches the just-upgraded npm
|
|
28
|
+
* version. Without this, the plugin in `~/.config/opencode/plugins/bizar/`
|
|
29
|
+
* would lag one version behind npm and the BUGS.md "version skew"
|
|
19
30
|
* trap would bite.
|
|
20
31
|
*
|
|
21
32
|
* Exit codes:
|
|
22
33
|
* 0 — all requested updates succeeded (or none were requested)
|
|
23
|
-
* 1 — at least one update failed
|
|
34
|
+
* 1 — at least one update failed, or the user cancelled the kill
|
|
24
35
|
*/
|
|
25
36
|
|
|
26
37
|
import chalk from 'chalk';
|
|
27
|
-
import { execSync, spawnSync } from 'node:child_process';
|
|
28
|
-
import { existsSync } from 'node:fs';
|
|
29
|
-
import {
|
|
38
|
+
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
39
|
+
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
40
|
+
import { homedir } from 'node:os';
|
|
41
|
+
import { join, dirname } from 'node:path';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
|
|
44
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
+
const __dirname = dirname(__filename);
|
|
30
46
|
|
|
31
47
|
const PKG_MAIN = '@polderlabs/bizar';
|
|
48
|
+
const PKG_DASH = '@polderlabs/bizar-dash';
|
|
32
49
|
const PKG_PLUGIN = '@polderlabs/bizar-plugin';
|
|
33
50
|
|
|
51
|
+
// All known components, in the order they should be prompted + updated.
|
|
52
|
+
const COMPONENTS = ['opencode', 'bizar', 'dash', 'plugin'];
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Config paths (mirror the dashboard + service for consistency)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function bizarConfigDir() {
|
|
59
|
+
if (process.platform === 'win32') {
|
|
60
|
+
return process.env.APPDATA
|
|
61
|
+
? join(process.env.APPDATA, 'bizar')
|
|
62
|
+
: join(homedir(), '.config', 'bizar');
|
|
63
|
+
}
|
|
64
|
+
return process.env.XDG_CONFIG_HOME
|
|
65
|
+
? join(process.env.XDG_CONFIG_HOME, 'bizar')
|
|
66
|
+
: join(homedir(), '.config', 'bizar');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const BIZAR_HOME = bizarConfigDir();
|
|
70
|
+
const SERVICE_PID_FILE = join(BIZAR_HOME, 'service.pid');
|
|
71
|
+
const DASHBOARD_PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
|
|
72
|
+
const DASHBOARD_PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// PID helpers (exported for testability)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read and validate a PID file. Returns a live integer PID, or `null`
|
|
80
|
+
* if the file is missing / empty / contains a non-numeric value / the
|
|
81
|
+
* process is not running. Stale or corrupt PID files are removed.
|
|
82
|
+
*
|
|
83
|
+
* Exported so tests can exercise the cleanup behavior without going
|
|
84
|
+
* through the full update flow.
|
|
85
|
+
*/
|
|
86
|
+
export function readLivePid(pidFile) {
|
|
87
|
+
if (!existsSync(pidFile)) return null;
|
|
88
|
+
let raw;
|
|
89
|
+
try {
|
|
90
|
+
raw = readFileSync(pidFile, 'utf8').trim();
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!raw) {
|
|
95
|
+
// Empty / corrupt — clean it up so future checks are accurate.
|
|
96
|
+
try { rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const pid = parseInt(raw, 10);
|
|
100
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
101
|
+
try { rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 0);
|
|
106
|
+
return pid;
|
|
107
|
+
} catch {
|
|
108
|
+
// PID no longer alive — stale PID file.
|
|
109
|
+
try { rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Send SIGTERM, wait up to `timeoutMs`, then SIGKILL if still alive.
|
|
116
|
+
* Returns true if a signal was successfully delivered (or never needed).
|
|
117
|
+
*
|
|
118
|
+
* Implementation note: Linux PIDs are recycled immediately after a
|
|
119
|
+
* process dies, so `process.kill(pid, 0)` after a kill is an unreliable
|
|
120
|
+
* liveness check — the PID may already belong to a brand-new process.
|
|
121
|
+
* We treat any successful signal delivery as success; if SIGKILL is
|
|
122
|
+
* sent, the original process is certainly dead (uncatchable).
|
|
123
|
+
*
|
|
124
|
+
* Exported for testability.
|
|
125
|
+
*/
|
|
126
|
+
export function killAndWait(pid, { timeoutMs = 5000, label = 'process' } = {}) {
|
|
127
|
+
if (!pid) return true;
|
|
128
|
+
|
|
129
|
+
// Phase 1: graceful SIGTERM
|
|
130
|
+
let sigtermOk = false;
|
|
131
|
+
try {
|
|
132
|
+
process.kill(pid, 'SIGTERM');
|
|
133
|
+
sigtermOk = true;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err.code === 'ESRCH') return true; // already dead
|
|
136
|
+
console.log(chalk.yellow(` ! could not SIGTERM ${label} (pid ${pid}): ${err.message}`));
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Best-effort poll for graceful exit. Note: a SIGTERM-handling process
|
|
141
|
+
// (Express dashboard, Node sleeper) usually exits within ~100ms. We
|
|
142
|
+
// poll for up to `timeoutMs`; if anything responds to kill -0 it may be
|
|
143
|
+
// a recycled PID, so we don't treat that as "still our process".
|
|
144
|
+
const start = Date.now();
|
|
145
|
+
let sawExit = false;
|
|
146
|
+
while (Date.now() - start < timeoutMs) {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid, 0);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err.code === 'ESRCH') { sawExit = true; break; }
|
|
151
|
+
}
|
|
152
|
+
spawnSync('sleep', ['0.1']);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (sawExit) {
|
|
156
|
+
// Confirmed gone via ESRCH.
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Phase 2: escalate to SIGKILL. Even if `kill -0` still succeeds (PID
|
|
161
|
+
// recycled or process truly stuck), SIGKILL is uncatchable and the
|
|
162
|
+
// original process — if it was still our PID — is now dead.
|
|
163
|
+
try {
|
|
164
|
+
process.kill(pid, 'SIGKILL');
|
|
165
|
+
if (sigtermOk) {
|
|
166
|
+
console.log(chalk.yellow(` ! ${label} (pid ${pid}) did not exit gracefully; sent SIGKILL`));
|
|
167
|
+
}
|
|
168
|
+
// Brief settle for the kernel.
|
|
169
|
+
spawnSync('sleep', ['0.2']);
|
|
170
|
+
return true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err.code === 'ESRCH') return true;
|
|
173
|
+
console.log(chalk.red(` ✗ could not SIGKILL ${label} (pid ${pid}): ${err.message}`));
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
34
178
|
// ---------------------------------------------------------------------------
|
|
35
179
|
// Version helpers
|
|
36
180
|
// ---------------------------------------------------------------------------
|
|
@@ -41,8 +185,10 @@ const PKG_PLUGIN = '@polderlabs/bizar-plugin';
|
|
|
41
185
|
*/
|
|
42
186
|
function currentVersion(pkg) {
|
|
43
187
|
try {
|
|
44
|
-
const out = execSync(`npm ls -g ${pkg} --depth=0 --json`, {
|
|
45
|
-
|
|
188
|
+
const out = execSync(`npm ls -g ${pkg} --depth=0 --json`, {
|
|
189
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
190
|
+
timeout: 15000,
|
|
191
|
+
}).toString();
|
|
46
192
|
const parsed = JSON.parse(out);
|
|
47
193
|
const deps = parsed.dependencies ?? {};
|
|
48
194
|
return deps[pkg]?.version ?? null;
|
|
@@ -57,9 +203,10 @@ function currentVersion(pkg) {
|
|
|
57
203
|
*/
|
|
58
204
|
function latestVersion(pkg) {
|
|
59
205
|
try {
|
|
60
|
-
const out = execSync(`npm view ${pkg} version`, {
|
|
61
|
-
|
|
62
|
-
|
|
206
|
+
const out = execSync(`npm view ${pkg} version`, {
|
|
207
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
208
|
+
timeout: 15000,
|
|
209
|
+
}).toString().trim();
|
|
63
210
|
return out || null;
|
|
64
211
|
} catch {
|
|
65
212
|
return null;
|
|
@@ -76,12 +223,10 @@ function latestVersion(pkg) {
|
|
|
76
223
|
* Returns `{ ok: boolean, message: string }`.
|
|
77
224
|
*/
|
|
78
225
|
function updateOpencode() {
|
|
79
|
-
// Try `opencode upgrade` first — that's the canonical updater.
|
|
80
226
|
const r1 = spawnSync('opencode', ['upgrade'], { stdio: 'inherit' });
|
|
81
227
|
if (r1.status === 0) {
|
|
82
228
|
return { ok: true, message: 'opencode updated via `opencode upgrade`' };
|
|
83
229
|
}
|
|
84
|
-
// Fall back to npm. `opencode-ai` is the npm package name.
|
|
85
230
|
console.log(chalk.dim(' opencode upgrade not available; falling back to npm'));
|
|
86
231
|
const r2 = spawnSync('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit' });
|
|
87
232
|
if (r2.status === 0) {
|
|
@@ -90,26 +235,12 @@ function updateOpencode() {
|
|
|
90
235
|
return { ok: false, message: 'opencode update failed — try `opencode upgrade` manually' };
|
|
91
236
|
}
|
|
92
237
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
*/
|
|
96
|
-
function updateBizar() {
|
|
97
|
-
const r = spawnSync('npm', ['install', '-g', `${PKG_MAIN}@latest`], { stdio: 'inherit' });
|
|
238
|
+
function updatePackage(pkg) {
|
|
239
|
+
const r = spawnSync('npm', ['install', '-g', `${pkg}@latest`], { stdio: 'inherit' });
|
|
98
240
|
if (r.status !== 0) {
|
|
99
|
-
return { ok: false, message: `${
|
|
241
|
+
return { ok: false, message: `${pkg} update failed` };
|
|
100
242
|
}
|
|
101
|
-
return { ok: true, message: `${
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Update @polderlabs/bizar-plugin globally. Returns `{ ok, message }`.
|
|
106
|
-
*/
|
|
107
|
-
function updateBizarPlugin() {
|
|
108
|
-
const r = spawnSync('npm', ['install', '-g', `${PKG_PLUGIN}@latest`], { stdio: 'inherit' });
|
|
109
|
-
if (r.status !== 0) {
|
|
110
|
-
return { ok: false, message: `${PKG_PLUGIN} update failed` };
|
|
111
|
-
}
|
|
112
|
-
return { ok: true, message: `${PKG_PLUGIN} updated` };
|
|
243
|
+
return { ok: true, message: `${pkg} updated` };
|
|
113
244
|
}
|
|
114
245
|
|
|
115
246
|
// ---------------------------------------------------------------------------
|
|
@@ -122,8 +253,6 @@ function updateBizarPlugin() {
|
|
|
122
253
|
* fix for the BUGS.md "version skew" trap.
|
|
123
254
|
*/
|
|
124
255
|
function rerunInstallScript() {
|
|
125
|
-
// Find the package root via `npm root -g` + package name. This works
|
|
126
|
-
// whether the user installed via `npm install -g` or `npx`.
|
|
127
256
|
let globalRoot;
|
|
128
257
|
try {
|
|
129
258
|
globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
@@ -142,12 +271,10 @@ function rerunInstallScript() {
|
|
|
142
271
|
}
|
|
143
272
|
return { ok: false, message: 'setup rerun failed' };
|
|
144
273
|
}
|
|
145
|
-
|
|
146
274
|
const installSh = join(pkgRoot, 'install.sh');
|
|
147
275
|
if (process.platform === 'win32' || !existsSync(installSh)) {
|
|
148
276
|
return { ok: false, message: 'could not locate a compatible setup script to re-run' };
|
|
149
277
|
}
|
|
150
|
-
|
|
151
278
|
console.log(chalk.dim(`\n Re-running install script at ${installSh}...`));
|
|
152
279
|
const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
|
|
153
280
|
if (r.status !== 0) {
|
|
@@ -157,19 +284,141 @@ function rerunInstallScript() {
|
|
|
157
284
|
}
|
|
158
285
|
|
|
159
286
|
// ---------------------------------------------------------------------------
|
|
160
|
-
//
|
|
287
|
+
// Instance detection + kill
|
|
161
288
|
// ---------------------------------------------------------------------------
|
|
162
289
|
|
|
163
290
|
/**
|
|
164
|
-
*
|
|
165
|
-
* Returns
|
|
291
|
+
* Detect live Bizar instances by reading the well-known PID files.
|
|
292
|
+
* Returns:
|
|
293
|
+
* { service: { pid, label } | null,
|
|
294
|
+
* dashboard: { pid, port } | null,
|
|
295
|
+
* other: Array<{ pid, cmd }> }
|
|
166
296
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
297
|
+
function detectInstances() {
|
|
298
|
+
const servicePid = readLivePid(SERVICE_PID_FILE);
|
|
299
|
+
const dashboardPid = readLivePid(DASHBOARD_PID_FILE);
|
|
300
|
+
let port = null;
|
|
301
|
+
if (dashboardPid && existsSync(DASHBOARD_PORT_FILE)) {
|
|
302
|
+
try {
|
|
303
|
+
port = parseInt(readFileSync(DASHBOARD_PORT_FILE, 'utf8').trim(), 10) || null;
|
|
304
|
+
} catch { /* ignore */ }
|
|
170
305
|
}
|
|
171
|
-
|
|
172
|
-
|
|
306
|
+
return {
|
|
307
|
+
service: servicePid ? { pid: servicePid, label: `bizar service (pid ${servicePid})` } : null,
|
|
308
|
+
dashboard: dashboardPid ? { pid: dashboardPid, port, label: `bizar-dash web dashboard (pid ${dashboardPid}${port ? `, port ${port}` : ''})` } : null,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Ask the user to confirm killing the listed instances. Returns true if
|
|
314
|
+
* they confirmed, false if they cancelled. Skipped entirely with `assumeYes`.
|
|
315
|
+
*/
|
|
316
|
+
async function confirmKill(instances, { assumeYes } = {}) {
|
|
317
|
+
const lines = [];
|
|
318
|
+
if (instances.service) lines.push(` • ${instances.service.label} — background schedule runner`);
|
|
319
|
+
if (instances.dashboard) lines.push(` • ${instances.dashboard.label} — web UI + API`);
|
|
320
|
+
if (lines.length === 0) return true;
|
|
321
|
+
if (assumeYes) return true;
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log(chalk.bold.yellow(' ⚠ Running Bizar instances detected:'));
|
|
324
|
+
for (const l of lines) console.log(chalk.yellow(l));
|
|
325
|
+
console.log('');
|
|
326
|
+
console.log(chalk.yellow(' These must be stopped before npm can replace the on-disk files.'));
|
|
327
|
+
console.log(chalk.yellow(' Killing them will close any open web tabs / TUI sessions.'));
|
|
328
|
+
console.log('');
|
|
329
|
+
// Best-effort interactive confirmation. Non-TTY (CI / piped input) skips the
|
|
330
|
+
// prompt and aborts — callers should pass `--yes` explicitly in that case.
|
|
331
|
+
if (!process.stdin.isTTY) {
|
|
332
|
+
console.log(chalk.red(' Non-interactive shell detected; rerun with --yes to confirm the kill, or stop the processes manually.'));
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const inquirer = await import('inquirer');
|
|
337
|
+
const { ok } = await inquirer.default.prompt([
|
|
338
|
+
{
|
|
339
|
+
type: 'confirm',
|
|
340
|
+
name: 'ok',
|
|
341
|
+
message: 'Kill the running instance(s) and continue?',
|
|
342
|
+
default: true,
|
|
343
|
+
},
|
|
344
|
+
]);
|
|
345
|
+
return Boolean(ok);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.log(chalk.red(` ! inquirer unavailable: ${err.message}`));
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function killInstances(instances) {
|
|
353
|
+
const out = [];
|
|
354
|
+
if (instances.service) {
|
|
355
|
+
const ok = killAndWait(instances.service.pid, { label: 'bizar service' });
|
|
356
|
+
out.push({ name: 'service', ok });
|
|
357
|
+
if (ok) {
|
|
358
|
+
try { rmSync(SERVICE_PID_FILE, { force: true }); } catch { /* ignore */ }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (instances.dashboard) {
|
|
362
|
+
const ok = killAndWait(instances.dashboard.pid, { label: 'bizar-dash' });
|
|
363
|
+
out.push({ name: 'dashboard', ok });
|
|
364
|
+
if (ok) {
|
|
365
|
+
try { rmSync(DASHBOARD_PID_FILE, { force: true }); } catch { /* ignore */ }
|
|
366
|
+
try { rmSync(DASHBOARD_PORT_FILE, { force: true }); } catch { /* ignore */ }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Restart the dashboard after the update completes
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Spawn a fresh dashboard process detached, returning the new PID. The
|
|
378
|
+
* dashboard will use the just-updated @polderlabs/bizar-dash code from
|
|
379
|
+
* the global npm install.
|
|
380
|
+
*/
|
|
381
|
+
function spawnFreshDashboard({ port } = {}) {
|
|
382
|
+
let globalRoot;
|
|
383
|
+
try {
|
|
384
|
+
globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
385
|
+
.toString()
|
|
386
|
+
.trim();
|
|
387
|
+
} catch {
|
|
388
|
+
return { ok: false, message: 'could not locate npm global root' };
|
|
389
|
+
}
|
|
390
|
+
const dashBin = join(globalRoot, ...PKG_DASH.split('/'), 'src', 'cli.mjs');
|
|
391
|
+
if (!existsSync(dashBin)) {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
message: `dashboard binary not found at ${dashBin} (was @polderlabs/bizar-dash installed?)`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
399
|
+
} catch { /* ignore */ }
|
|
400
|
+
const args = [dashBin, 'start', '--bg'];
|
|
401
|
+
if (port) args.push(`--port=${port}`);
|
|
402
|
+
try {
|
|
403
|
+
const child = spawn(process.execPath, args, {
|
|
404
|
+
detached: true,
|
|
405
|
+
stdio: 'ignore',
|
|
406
|
+
env: { ...process.env, BIZAR_AUTO_RESPAWN: '1' },
|
|
407
|
+
});
|
|
408
|
+
child.on('error', () => { /* ignore */ });
|
|
409
|
+
child.unref();
|
|
410
|
+
return { ok: true, message: `dashboard re-spawned (pid ${child.pid})` };
|
|
411
|
+
} catch (err) {
|
|
412
|
+
return { ok: false, message: `dashboard re-spawn failed: ${err.message}` };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// Prompt helpers
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
async function promptForUpdates(forceAll) {
|
|
421
|
+
if (forceAll) return new Set(COMPONENTS);
|
|
173
422
|
const inquirer = await import('inquirer');
|
|
174
423
|
const { selections } = await inquirer.default.prompt([
|
|
175
424
|
{
|
|
@@ -179,6 +428,7 @@ async function promptForUpdates(forceAll) {
|
|
|
179
428
|
choices: [
|
|
180
429
|
{ name: 'opencode (the opencode CLI itself)', value: 'opencode', checked: true },
|
|
181
430
|
{ name: `bizar (${PKG_MAIN})`, value: 'bizar', checked: true },
|
|
431
|
+
{ name: `bizar-dash (${PKG_DASH}) — web dashboard`, value: 'dash', checked: true },
|
|
182
432
|
{ name: `plugin (${PKG_PLUGIN})`, value: 'plugin', checked: true },
|
|
183
433
|
],
|
|
184
434
|
},
|
|
@@ -190,29 +440,60 @@ async function promptForUpdates(forceAll) {
|
|
|
190
440
|
// Main entry point
|
|
191
441
|
// ---------------------------------------------------------------------------
|
|
192
442
|
|
|
193
|
-
/**
|
|
194
|
-
* Run the update subcommand.
|
|
195
|
-
*
|
|
196
|
-
* @param {string[]} subargs - arguments after `bizar update`
|
|
197
|
-
*/
|
|
198
443
|
export async function runUpdate(subargs = []) {
|
|
199
444
|
console.log(chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE ᚦ\n'));
|
|
200
445
|
|
|
201
|
-
//
|
|
446
|
+
// Parse flags
|
|
447
|
+
const assumeYes = subargs.includes('--yes') || subargs.includes('-y') || subargs.includes('--force');
|
|
448
|
+
const restartAfter = !subargs.includes('--no-restart');
|
|
449
|
+
const forceAll = subargs.includes('--all');
|
|
450
|
+
|
|
451
|
+
// 1. Detect running instances BEFORE doing anything else.
|
|
452
|
+
const instances = detectInstances();
|
|
453
|
+
const runningCount = (instances.service ? 1 : 0) + (instances.dashboard ? 1 : 0);
|
|
454
|
+
|
|
455
|
+
if (runningCount > 0) {
|
|
456
|
+
const ok = await confirmKill(instances, { assumeYes });
|
|
457
|
+
if (!ok) {
|
|
458
|
+
console.log(chalk.yellow('\n Update cancelled — instances still running.'));
|
|
459
|
+
console.log(chalk.dim(' Stop them with `bizar service stop` and `bizar dashboard stop`, then retry.'));
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
console.log(chalk.cyan('\n Stopping running instances...'));
|
|
463
|
+
const kills = killInstances(instances);
|
|
464
|
+
for (const k of kills) {
|
|
465
|
+
const marker = k.ok ? chalk.green('✓') : chalk.red('✗');
|
|
466
|
+
console.log(` ${marker} ${k.name} stopped`);
|
|
467
|
+
}
|
|
468
|
+
// Give the kernel a moment to release any open file handles on the
|
|
469
|
+
// npm-global directory before npm tries to replace files.
|
|
470
|
+
spawnSync('sleep', ['0.5']);
|
|
471
|
+
} else {
|
|
472
|
+
console.log(chalk.dim(' No running Bizar instances detected.'));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 2. Show installed vs. latest versions.
|
|
202
476
|
const cur = {
|
|
203
477
|
opencode: currentVersion('opencode-ai'),
|
|
204
478
|
bizar: currentVersion(PKG_MAIN),
|
|
479
|
+
dash: currentVersion(PKG_DASH),
|
|
205
480
|
plugin: currentVersion(PKG_PLUGIN),
|
|
206
481
|
};
|
|
207
482
|
const latest = {
|
|
208
483
|
opencode: latestVersion('opencode-ai'),
|
|
209
484
|
bizar: latestVersion(PKG_MAIN),
|
|
485
|
+
dash: latestVersion(PKG_DASH),
|
|
210
486
|
plugin: latestVersion(PKG_PLUGIN),
|
|
211
487
|
};
|
|
212
488
|
|
|
489
|
+
console.log('');
|
|
213
490
|
console.log(' Installed vs. latest:');
|
|
214
|
-
for (const k of
|
|
215
|
-
const label =
|
|
491
|
+
for (const k of COMPONENTS) {
|
|
492
|
+
const label =
|
|
493
|
+
k === 'plugin' ? PKG_PLUGIN :
|
|
494
|
+
k === 'dash' ? PKG_DASH :
|
|
495
|
+
k === 'bizar' ? PKG_MAIN :
|
|
496
|
+
'opencode-ai';
|
|
216
497
|
const c = cur[k] ?? '(not installed)';
|
|
217
498
|
const l = latest[k] ?? '(unknown)';
|
|
218
499
|
const same = c === l;
|
|
@@ -220,19 +501,18 @@ export async function runUpdate(subargs = []) {
|
|
|
220
501
|
}
|
|
221
502
|
console.log('');
|
|
222
503
|
|
|
223
|
-
// Decide what to update.
|
|
504
|
+
// 3. Decide what to update.
|
|
224
505
|
let selected;
|
|
225
|
-
if (
|
|
226
|
-
selected = new Set(
|
|
506
|
+
if (forceAll || assumeYes) {
|
|
507
|
+
selected = new Set(COMPONENTS);
|
|
227
508
|
} else if (subargs.length === 0) {
|
|
228
509
|
selected = await promptForUpdates(false);
|
|
229
510
|
} else {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
selected = new Set(subargs.filter((a) => valid.has(a)));
|
|
511
|
+
const valid = new Set(COMPONENTS);
|
|
512
|
+
selected = new Set(subargs.filter((a) => !a.startsWith('-') && valid.has(a)));
|
|
233
513
|
if (selected.size === 0) {
|
|
234
514
|
console.log(chalk.yellow(` No valid components selected from: ${subargs.join(' ')}`));
|
|
235
|
-
console.log(chalk.dim(' Valid components: opencode, bizar, plugin'));
|
|
515
|
+
console.log(chalk.dim(' Valid components: opencode, bizar, dash, plugin'));
|
|
236
516
|
console.log(chalk.dim(' Run `bizar update --help` for usage.'));
|
|
237
517
|
process.exit(1);
|
|
238
518
|
}
|
|
@@ -252,15 +532,18 @@ export async function runUpdate(subargs = []) {
|
|
|
252
532
|
}
|
|
253
533
|
if (selected.has('bizar')) {
|
|
254
534
|
console.log(chalk.bold(` → ${PKG_MAIN}`));
|
|
255
|
-
results.push(['bizar',
|
|
535
|
+
results.push(['bizar', updatePackage(PKG_MAIN)]);
|
|
536
|
+
}
|
|
537
|
+
if (selected.has('dash')) {
|
|
538
|
+
console.log(chalk.bold(` → ${PKG_DASH}`));
|
|
539
|
+
results.push(['dash', updatePackage(PKG_DASH)]);
|
|
256
540
|
}
|
|
257
541
|
if (selected.has('plugin')) {
|
|
258
542
|
console.log(chalk.bold(` → ${PKG_PLUGIN}`));
|
|
259
|
-
results.push(['plugin',
|
|
543
|
+
results.push(['plugin', updatePackage(PKG_PLUGIN)]);
|
|
260
544
|
}
|
|
261
545
|
|
|
262
|
-
// Re-run the install script if anything
|
|
263
|
-
// plugin source matches the registry.
|
|
546
|
+
// 4. Re-run the install script if anything relevant changed.
|
|
264
547
|
const anySuccess = results.some(([, r]) => r.ok);
|
|
265
548
|
if (anySuccess && (selected.has('bizar') || selected.has('plugin'))) {
|
|
266
549
|
console.log('');
|
|
@@ -273,6 +556,20 @@ export async function runUpdate(subargs = []) {
|
|
|
273
556
|
}
|
|
274
557
|
}
|
|
275
558
|
|
|
559
|
+
// 5. Restart the dashboard if it was running before the update.
|
|
560
|
+
if (restartAfter && instances.dashboard && (selected.has('bizar') || selected.has('dash'))) {
|
|
561
|
+
console.log('');
|
|
562
|
+
console.log(chalk.cyan(' Restarting dashboard with the new code...'));
|
|
563
|
+
const res = spawnFreshDashboard({ port: instances.dashboard.port || undefined });
|
|
564
|
+
if (res.ok) {
|
|
565
|
+
console.log(chalk.green(` ✓ ${res.message}`));
|
|
566
|
+
} else {
|
|
567
|
+
console.log(chalk.yellow(` ⚠ ${res.message}`));
|
|
568
|
+
console.log(chalk.dim(' Start it manually with `bizar-dash start --bg`.'));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 6. Summary
|
|
276
573
|
console.log('');
|
|
277
574
|
console.log(' Summary:');
|
|
278
575
|
for (const [name, r] of results) {
|
|
@@ -286,4 +583,4 @@ export async function runUpdate(subargs = []) {
|
|
|
286
583
|
process.exit(1);
|
|
287
584
|
}
|
|
288
585
|
console.log(chalk.green('\n ✓ Update complete\n'));
|
|
289
|
-
}
|
|
586
|
+
}
|
package/config/AGENTS.md
CHANGED
|
@@ -283,6 +283,36 @@ The Hindsight MCP server is already configured. All agents interact with it thro
|
|
|
283
283
|
|
|
284
284
|
---
|
|
285
285
|
|
|
286
|
+
## Graph Query (bizar graph)
|
|
287
|
+
|
|
288
|
+
Bizar integrates [graphify](https://github.com/safishamsi/graphify) for per-project knowledge graphs. When investigating a Bizar project, **query the graph before grepping raw files** — it's faster and surfaces structural relationships grep can't see.
|
|
289
|
+
|
|
290
|
+
### Where the graph lives
|
|
291
|
+
`.bizar/graph/` inside the project (git-trackable JSON + Markdown; cache and per-machine interpreter path are gitignored).
|
|
292
|
+
|
|
293
|
+
### How to query
|
|
294
|
+
From the project root, the user (or heimdall via `/init` or any other agent prompted by Odin) can run:
|
|
295
|
+
- `bizar graph status` — confirm the graph exists; print node/edge/community counts
|
|
296
|
+
- `bizar graph query "<concept>"` — find nodes related to a concept (BFS traversal)
|
|
297
|
+
- `bizar graph path "<A>" "<B>"` — shortest path between two concepts
|
|
298
|
+
- `bizar graph explain "<X>"` — all nodes related to X
|
|
299
|
+
- `bizar graph update` — incremental rebuild after editing source files
|
|
300
|
+
- `bizar graph build` — full rebuild (overwrites existing graph)
|
|
301
|
+
- `bizar graph watch` — foreground watcher (Ctrl-C to stop)
|
|
302
|
+
|
|
303
|
+
### When to use the graph
|
|
304
|
+
- Before reading a large file: `bizar graph explain "<module-name>"` to see what calls/uses it
|
|
305
|
+
- When mapping unfamiliar code: `bizar graph query "<feature>"` to find related concepts
|
|
306
|
+
- When debugging cross-module interactions: `bizar graph path "<symptom>" "<root-cause>"`
|
|
307
|
+
- Before grep: `bizar graph query "<term>"` first — the graph may already point you to the right file
|
|
308
|
+
|
|
309
|
+
### When NOT to use the graph
|
|
310
|
+
- The graph is stale (run `bizar graph update` first)
|
|
311
|
+
- graphify is not installed (init skipped graph step; user can install with `pip install graphifyy` then `bizar graph build`)
|
|
312
|
+
- The question is about runtime behavior, not source structure
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
286
316
|
---
|
|
287
317
|
|
|
288
318
|
## General Agent Baseline — Always-On Behavior
|
|
@@ -548,3 +578,26 @@ For Bizar-internal claims (citing files, lines, tool results), use file:line ref
|
|
|
548
578
|
|
|
549
579
|
This baseline covers: identity, refusal, tone, formatting, lists, user wellbeing, evenhandedness, mistakes, knowledge cutoff and research-first, MCP servers and skills, mandatory skill-read, file creation, file handling, search, copyright, harmful content, citations, images, memory privacy, execution, clarification, and communication. Every Bizar agent — Odin, Frigg, Vör, Mimir, Heimdall, Hermod, Thor, Baldr, Tyr, Vidarr, Forseti — must follow it.
|
|
550
580
|
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Parallel Execution Awareness
|
|
584
|
+
|
|
585
|
+
You may be dispatched by Odin as one of several agents running concurrently against the same working directory and the same git repository. Your sibling agents **cannot see you** and you **cannot see them**. Without discipline this leads to silent file overwrites, `.git/index.lock` collisions, lockfile corruption, and lost work.
|
|
586
|
+
|
|
587
|
+
### Hard rules when you have siblings (Odin tells you in the prompt)
|
|
588
|
+
|
|
589
|
+
1. **File scope is sacred.** Odin assigns you a scope. Only modify files inside it. If you need to touch something outside, STOP and report — do not improvise.
|
|
590
|
+
2. **No write-level git.** `git commit`, `push`, `merge`, `rebase`, `reset`, `clean`, `stash`, branch-switching `checkout`, and `pull --rebase` are FORBIDDEN for every agent except @hermod. Use `git status`, `git diff`, `git log`, and `git add` (scope files only) for context.
|
|
591
|
+
3. **Detect conflicts before they happen.** Before writing a file, run `git diff --name-only` and confirm the file is not in a sibling's scope. If it has changed since you started, STOP and report.
|
|
592
|
+
4. **`.git/index.lock` is a sibling's signal.** If you see it, wait 2-3 seconds and retry. If it persists, STOP and report. Do not delete the lock file.
|
|
593
|
+
5. **Lockfiles and root configs are shared.** `package.json`, `package-lock.json`, `tsconfig.json`, `vite.config.*`, `Dockerfile`, CI configs — only ONE agent in a batch should touch these. If Odin did not assign them to you, treat as READ-ONLY.
|
|
594
|
+
6. **Report parallel context in your final summary.** State "siblings: ..." and any conflicts observed.
|
|
595
|
+
|
|
596
|
+
### Default behavior when Odin does NOT mention siblings
|
|
597
|
+
|
|
598
|
+
- You may work normally.
|
|
599
|
+
- Still avoid `git commit`/`push`/`merge`/`rebase`/`reset`/`clean`/`stash` unless explicitly asked. Default to read-only git unless the user/Odin explicitly requests a write operation. When in doubt, leave git work to @hermod.
|
|
600
|
+
|
|
601
|
+
### Why this exists
|
|
602
|
+
The harness shares one `.git/` directory across all parallel sessions. Two simultaneous `git commit` calls race on the index lock. Two agents writing the same file = silent last-writer-wins data loss. Discipline now is cheaper than recovery later.
|
|
603
|
+
|