@polderlabs/bizar 3.7.2 → 3.7.3
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 +32 -10
- package/cli/update.mjs +371 -74
- package/package.json +1 -1
package/cli/bin.mjs
CHANGED
|
@@ -157,18 +157,40 @@ function showInstallHelp() {
|
|
|
157
157
|
|
|
158
158
|
function showUpdateHelp() {
|
|
159
159
|
console.log(`
|
|
160
|
-
bizar update — Update opencode, bizar, and/or bizar-plugin
|
|
160
|
+
bizar update — Update opencode, bizar, bizar-dash, and/or bizar-plugin
|
|
161
161
|
|
|
162
162
|
Usage:
|
|
163
|
-
bizar update
|
|
164
|
-
bizar update --all
|
|
165
|
-
bizar update opencode
|
|
166
|
-
bizar update
|
|
167
|
-
bizar update
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
163
|
+
bizar update Interactive prompt for components
|
|
164
|
+
bizar update --all Update every component
|
|
165
|
+
bizar update opencode bizar dash Update specific components
|
|
166
|
+
bizar update --all --yes Update everything + auto-kill running instances
|
|
167
|
+
bizar update --no-restart Don't auto-restart the dashboard after update
|
|
168
|
+
bizar update --help Show this help
|
|
169
|
+
|
|
170
|
+
Components:
|
|
171
|
+
opencode the opencode CLI itself
|
|
172
|
+
bizar @polderlabs/bizar (this CLI + installer + agents + rules)
|
|
173
|
+
dash @polderlabs/bizar-dash (web dashboard + TUI)
|
|
174
|
+
plugin @polderlabs/bizar-plugin (opencode plugin)
|
|
175
|
+
|
|
176
|
+
Behavior:
|
|
177
|
+
• Detects running Bizar instances (background service daemon, web
|
|
178
|
+
dashboard) by reading ~/.config/bizar/{service,dashboard}.pid and
|
|
179
|
+
cleaning up any stale or empty PID files.
|
|
180
|
+
• Warns the user explicitly before killing each instance. Pass
|
|
181
|
+
--yes / -y / --force to skip the confirmation (required for
|
|
182
|
+
non-interactive shells).
|
|
183
|
+
• Sends SIGTERM, waits up to 5s, escalates to SIGKILL if needed.
|
|
184
|
+
• Re-runs the install script so the deployed plugin source matches
|
|
185
|
+
the just-upgraded npm version (avoids the version-skew trap).
|
|
186
|
+
• If the dashboard was running and bizar / bizar-dash were updated,
|
|
187
|
+
spawns a fresh detached dashboard process with the new code
|
|
188
|
+
(skipped with --no-restart).
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
bizar update --all --yes Headless full update + restart
|
|
192
|
+
bizar update dash Update only the dashboard
|
|
193
|
+
bizar update plugin --no-restart Plugin-only, leave dashboard alone
|
|
172
194
|
`);
|
|
173
195
|
}
|
|
174
196
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.3",
|
|
4
4
|
"description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|