@polderlabs/bizar 4.4.6 → 4.4.8
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/main-CEazNxxy.js +347 -0
- package/bizar-dash/dist/assets/main-CEazNxxy.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-CQTXbuHq.js → mobile-DSb-t42Y.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-CQTXbuHq.js.map → mobile-DSb-t42Y.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-CaZ0gEeJ.js → mobile-Dl1q7Cyq.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-CaZ0gEeJ.js.map → mobile-Dl1q7Cyq.js.map} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +13 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +240 -9
- package/cli/bin.mjs +39 -30
- package/cli/install.mjs +13 -51
- package/cli/provision.mjs +967 -0
- package/cli/update.mjs +28 -878
- package/config/skills/glyph/SKILL.md +126 -25
- package/install.sh +226 -439
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-C4Dq4wTz.js +0 -347
- package/bizar-dash/dist/assets/main-C4Dq4wTz.js.map +0 -1
package/cli/update.mjs
CHANGED
|
@@ -1,879 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* update.mjs — `bizar update` subcommand.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* trap would bite.
|
|
31
|
-
*
|
|
32
|
-
* Exit codes:
|
|
33
|
-
* 0 — all requested updates succeeded (or none were requested)
|
|
34
|
-
* 1 — at least one update failed, or the user cancelled the kill
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import chalk from 'chalk';
|
|
38
|
-
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
39
|
-
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
40
|
-
import { cp } from 'node:fs/promises';
|
|
41
|
-
import { homedir } from 'node:os';
|
|
42
|
-
import { join, dirname } from 'node:path';
|
|
43
|
-
import { fileURLToPath } from 'node:url';
|
|
44
|
-
import { checkHeadsUps, findBizarDir } from './heads-up.mjs';
|
|
45
|
-
|
|
46
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
47
|
-
const __dirname = dirname(__filename);
|
|
48
|
-
const REPO_ROOT = join(__dirname, '..');
|
|
49
|
-
|
|
50
|
-
// v4.4.5 — The plugin and dashboard are both shipped inside @polderlabs/bizar.
|
|
51
|
-
// No separate @polderlabs/bizar-plugin or @polderlabs/bizar-dash packages.
|
|
52
|
-
// Updating the main package updates everything.
|
|
53
|
-
const PKG_MAIN = '@polderlabs/bizar';
|
|
54
|
-
|
|
55
|
-
// All known components, in the order they should be prompted + updated.
|
|
56
|
-
const COMPONENTS = ['opencode', 'bizar'];
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Config paths (mirror the dashboard + service for consistency)
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
function bizarConfigDir() {
|
|
63
|
-
if (process.platform === 'win32') {
|
|
64
|
-
return process.env.APPDATA
|
|
65
|
-
? join(process.env.APPDATA, 'bizar')
|
|
66
|
-
: join(homedir(), '.config', 'bizar');
|
|
67
|
-
}
|
|
68
|
-
return process.env.XDG_CONFIG_HOME
|
|
69
|
-
? join(process.env.XDG_CONFIG_HOME, 'bizar')
|
|
70
|
-
: join(homedir(), '.config', 'bizar');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const BIZAR_HOME = bizarConfigDir();
|
|
74
|
-
const SERVICE_PID_FILE = join(BIZAR_HOME, 'service.pid');
|
|
75
|
-
const DASHBOARD_PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
|
|
76
|
-
const DASHBOARD_PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// PID helpers (exported for testability)
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Read and validate a PID file. Returns a live integer PID, or `null`
|
|
84
|
-
* if the file is missing / empty / contains a non-numeric value / the
|
|
85
|
-
* process is not running. Stale or corrupt PID files are removed.
|
|
86
|
-
*
|
|
87
|
-
* Exported so tests can exercise the cleanup behavior without going
|
|
88
|
-
* through the full update flow.
|
|
89
|
-
*/
|
|
90
|
-
export function readLivePid(pidFile) {
|
|
91
|
-
if (!existsSync(pidFile)) return null;
|
|
92
|
-
let raw;
|
|
93
|
-
try {
|
|
94
|
-
raw = readFileSync(pidFile, 'utf8').trim();
|
|
95
|
-
} catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
if (!raw) {
|
|
99
|
-
// Empty / corrupt — clean it up so future checks are accurate.
|
|
100
|
-
try { rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
const pid = parseInt(raw, 10);
|
|
104
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
105
|
-
try { rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
process.kill(pid, 0);
|
|
110
|
-
return pid;
|
|
111
|
-
} catch {
|
|
112
|
-
// PID no longer alive — stale PID file.
|
|
113
|
-
try { rmSync(pidFile, { force: true }); } catch { /* ignore */ }
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Send SIGTERM, wait up to `timeoutMs`, then SIGKILL if still alive.
|
|
120
|
-
* Returns true if a signal was successfully delivered (or never needed).
|
|
121
|
-
*
|
|
122
|
-
* Implementation note: Linux PIDs are recycled immediately after a
|
|
123
|
-
* process dies, so `process.kill(pid, 0)` after a kill is an unreliable
|
|
124
|
-
* liveness check — the PID may already belong to a brand-new process.
|
|
125
|
-
* We treat any successful signal delivery as success; if SIGKILL is
|
|
126
|
-
* sent, the original process is certainly dead (uncatchable).
|
|
127
|
-
*
|
|
128
|
-
* Cross-platform note: Node.js 14+ maps `process.kill(pid)` without an
|
|
129
|
-
* explicit signal to the platform-appropriate default (`SIGTERM` on
|
|
130
|
-
* POSIX, `TerminateProcess` on Windows). We drop the signal argument so
|
|
131
|
-
* the same code works on both platforms. For the forced-kill phase on
|
|
132
|
-
* Windows we use `taskkill /F /PID <pid>` because `SIGKILL` is not
|
|
133
|
-
* delivered the same way there.
|
|
134
|
-
*
|
|
135
|
-
* Exported for testability.
|
|
136
|
-
*/
|
|
137
|
-
export async function killAndWait(pid, { timeoutMs = 5000, label = 'process' } = {}) {
|
|
138
|
-
if (!pid) return true;
|
|
139
|
-
|
|
140
|
-
// Phase 1: graceful SIGTERM (or platform default on Windows)
|
|
141
|
-
let sigtermOk = false;
|
|
142
|
-
try {
|
|
143
|
-
process.kill(pid);
|
|
144
|
-
sigtermOk = true;
|
|
145
|
-
} catch (err) {
|
|
146
|
-
if (err.code === 'ESRCH') return true; // already dead
|
|
147
|
-
console.log(chalk.yellow(` ! could not signal ${label} (pid ${pid}): ${err.message}`));
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Best-effort poll for graceful exit. Note: a signal-handling process
|
|
152
|
-
// (Express dashboard, Node sleeper) usually exits within ~100ms. We
|
|
153
|
-
// poll for up to `timeoutMs`; if anything responds to kill -0 it may be
|
|
154
|
-
// a recycled PID, so we don't treat that as "still our process".
|
|
155
|
-
const start = Date.now();
|
|
156
|
-
let sawExit = false;
|
|
157
|
-
while (Date.now() - start < timeoutMs) {
|
|
158
|
-
try {
|
|
159
|
-
process.kill(pid, 0);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
if (err.code === 'ESRCH') { sawExit = true; break; }
|
|
162
|
-
}
|
|
163
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (sawExit) {
|
|
167
|
-
// Confirmed gone via ESRCH.
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Phase 2: escalate to forced kill. Even if `kill -0` still succeeds
|
|
172
|
-
// (PID recycled or process truly stuck), SIGKILL is uncatchable and
|
|
173
|
-
// the original process — if it was still our PID — is now dead.
|
|
174
|
-
// On Windows, use `taskkill /F` because POSIX SIGKILL semantics don't
|
|
175
|
-
// exist there.
|
|
176
|
-
try {
|
|
177
|
-
if (process.platform === 'win32') {
|
|
178
|
-
spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore' });
|
|
179
|
-
} else {
|
|
180
|
-
process.kill(pid, 'SIGKILL');
|
|
181
|
-
}
|
|
182
|
-
if (sigtermOk) {
|
|
183
|
-
console.log(chalk.yellow(` ! ${label} (pid ${pid}) did not exit gracefully; sent forced kill`));
|
|
184
|
-
}
|
|
185
|
-
// Brief settle for the kernel.
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
187
|
-
return true;
|
|
188
|
-
} catch (err) {
|
|
189
|
-
if (err.code === 'ESRCH') return true;
|
|
190
|
-
console.log(chalk.red(` ✗ could not force-kill ${label} (pid ${pid}): ${err.message}`));
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
// Version helpers
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Read the currently installed version of an npm package.
|
|
201
|
-
* Returns `null` if not installed globally.
|
|
202
|
-
*/
|
|
203
|
-
function currentVersion(pkg) {
|
|
204
|
-
try {
|
|
205
|
-
const out = execSync(`npm ls -g ${pkg} --depth=0 --json`, {
|
|
206
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
207
|
-
timeout: 15000,
|
|
208
|
-
}).toString();
|
|
209
|
-
const parsed = JSON.parse(out);
|
|
210
|
-
const deps = parsed.dependencies ?? {};
|
|
211
|
-
return deps[pkg]?.version ?? null;
|
|
212
|
-
} catch {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Read the latest version of an npm package from the registry.
|
|
219
|
-
* Returns `null` if the registry is unreachable or the package is not published.
|
|
220
|
-
*/
|
|
221
|
-
function latestVersion(pkg) {
|
|
222
|
-
try {
|
|
223
|
-
const out = execSync(`npm view ${pkg} version`, {
|
|
224
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
225
|
-
timeout: 15000,
|
|
226
|
-
}).toString().trim();
|
|
227
|
-
return out || null;
|
|
228
|
-
} catch {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
// Update actions
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Update opencode. Tries `opencode upgrade` first (the upstream installer's
|
|
239
|
-
* own command); falls back to `npm install -g opencode-ai@latest`.
|
|
240
|
-
* Pass `{ dryRun: true }` to print what would run without executing.
|
|
241
|
-
* Returns `{ ok: boolean, message: string }`.
|
|
242
|
-
*/
|
|
243
|
-
function updateOpencode({ dryRun = false } = {}) {
|
|
244
|
-
if (dryRun) {
|
|
245
|
-
console.log(
|
|
246
|
-
chalk.dim(
|
|
247
|
-
' [dry-run] would run: opencode upgrade (fallback: npm install -g opencode-ai@latest)',
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
return { ok: true, message: '[dry-run] opencode' };
|
|
251
|
-
}
|
|
252
|
-
const r1 = spawnSync('opencode', ['upgrade'], { stdio: 'inherit' });
|
|
253
|
-
if (r1.status === 0) {
|
|
254
|
-
return { ok: true, message: 'opencode updated via `opencode upgrade`' };
|
|
255
|
-
}
|
|
256
|
-
console.log(chalk.dim(' opencode upgrade not available; falling back to npm'));
|
|
257
|
-
const r2 = spawnSync('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit' });
|
|
258
|
-
if (r2.status === 0) {
|
|
259
|
-
return { ok: true, message: 'opencode updated via npm' };
|
|
260
|
-
}
|
|
261
|
-
return { ok: false, message: 'opencode update failed — try `opencode upgrade` manually' };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function updatePackage(pkg, { dryRun = false } = {}) {
|
|
265
|
-
if (dryRun) {
|
|
266
|
-
console.log(
|
|
267
|
-
chalk.dim(` [dry-run] would run: npm install -g ${pkg}@latest`),
|
|
268
|
-
);
|
|
269
|
-
return { ok: true, message: `[dry-run] ${pkg}` };
|
|
270
|
-
}
|
|
271
|
-
const r = spawnSync('npm', ['install', '-g', `${pkg}@latest`], { stdio: 'inherit' });
|
|
272
|
-
if (r.status !== 0) {
|
|
273
|
-
return { ok: false, message: `${pkg} update failed` };
|
|
274
|
-
}
|
|
275
|
-
return { ok: true, message: `${pkg} updated` };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ---------------------------------------------------------------------------
|
|
279
|
-
// Re-run the install script so the locally deployed plugin matches npm
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* After updating the npm packages, re-run the install script so the
|
|
284
|
-
* plugin source on disk matches the just-installed version. This is the
|
|
285
|
-
* fix for the BUGS.md "version skew" trap.
|
|
286
|
-
*/
|
|
287
|
-
function rerunInstallScript() {
|
|
288
|
-
let globalRoot;
|
|
289
|
-
try {
|
|
290
|
-
globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
291
|
-
.toString()
|
|
292
|
-
.trim();
|
|
293
|
-
} catch {
|
|
294
|
-
return { ok: false, message: 'could not locate npm global root; skipping install-script rerun' };
|
|
295
|
-
}
|
|
296
|
-
const pkgRoot = join(globalRoot, ...PKG_MAIN.split('/'));
|
|
297
|
-
// v3.20.11: prefer the canonical `install.sh` script over `cli/bin.mjs
|
|
298
|
-
// --setup`. The bash script is the single source of truth (it handles
|
|
299
|
-
// system deps, chrome-headless-shell runtime libs, browser-harness,
|
|
300
|
-
// opencode.json merging, etc.); the --setup path is a legacy fallback
|
|
301
|
-
// for environments where bash isn't on PATH.
|
|
302
|
-
const installSh = join(pkgRoot, 'install.sh');
|
|
303
|
-
if (process.platform === 'win32') {
|
|
304
|
-
// On Windows without WSL, bash isn't available. The bash script has
|
|
305
|
-
// a Windows fallback that uses npm-based install paths. Fall through
|
|
306
|
-
// to the bin.mjs --setup path, which still does the agent copy +
|
|
307
|
-
// plugin-from-global install on Windows.
|
|
308
|
-
console.log(chalk.dim(' Windows: using bin.mjs --setup path (bash not available)'));
|
|
309
|
-
} else if (existsSync(installSh)) {
|
|
310
|
-
console.log(chalk.dim(`\n Re-running install script at ${installSh}...`));
|
|
311
|
-
const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
|
|
312
|
-
if (r.status !== 0) {
|
|
313
|
-
return { ok: false, message: 'install script rerun failed' };
|
|
314
|
-
}
|
|
315
|
-
return { ok: true, message: 'install script re-run' };
|
|
316
|
-
} else {
|
|
317
|
-
console.log(chalk.dim(' install.sh not present — falling back to bin.mjs --setup'));
|
|
318
|
-
}
|
|
319
|
-
const binPath = join(pkgRoot, 'cli', 'bin.mjs');
|
|
320
|
-
if (!existsSync(binPath)) {
|
|
321
|
-
return { ok: false, message: 'could not locate a compatible setup script to re-run' };
|
|
322
|
-
}
|
|
323
|
-
console.log(chalk.dim(`\n Re-running setup via ${binPath} --setup...`));
|
|
324
|
-
const r = spawnSync(process.execPath, [binPath, '--setup'], { stdio: 'inherit' });
|
|
325
|
-
if (r.status === 0) {
|
|
326
|
-
return { ok: true, message: 'setup re-run' };
|
|
327
|
-
}
|
|
328
|
-
return { ok: false, message: 'setup rerun failed' };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
// Instance detection + kill
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Detect live Bizar instances by reading the well-known PID files.
|
|
337
|
-
* Returns:
|
|
338
|
-
* { service: { pid, label } | null,
|
|
339
|
-
* dashboard: { pid, port } | null,
|
|
340
|
-
* other: Array<{ pid, cmd }> }
|
|
341
|
-
*/
|
|
342
|
-
function detectInstances() {
|
|
343
|
-
const servicePid = readLivePid(SERVICE_PID_FILE);
|
|
344
|
-
const dashboardPid = readLivePid(DASHBOARD_PID_FILE);
|
|
345
|
-
let port = null;
|
|
346
|
-
if (dashboardPid && existsSync(DASHBOARD_PORT_FILE)) {
|
|
347
|
-
try {
|
|
348
|
-
port = parseInt(readFileSync(DASHBOARD_PORT_FILE, 'utf8').trim(), 10) || null;
|
|
349
|
-
} catch { /* ignore */ }
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
service: servicePid ? { pid: servicePid, label: `bizar service (pid ${servicePid})` } : null,
|
|
353
|
-
dashboard: dashboardPid ? { pid: dashboardPid, port, label: `bizar-dash web dashboard (pid ${dashboardPid}${port ? `, port ${port}` : ''})` } : null,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Ask the user to confirm killing the listed instances. Returns true if
|
|
359
|
-
* they confirmed, false if they cancelled. Skipped entirely with `assumeYes`.
|
|
360
|
-
*/
|
|
361
|
-
async function confirmKill(instances, { assumeYes } = {}) {
|
|
362
|
-
const lines = [];
|
|
363
|
-
if (instances.service) lines.push(` • ${instances.service.label} — background schedule runner`);
|
|
364
|
-
if (instances.dashboard) lines.push(` • ${instances.dashboard.label} — web UI + API`);
|
|
365
|
-
if (lines.length === 0) return true;
|
|
366
|
-
if (assumeYes) return true;
|
|
367
|
-
console.log('');
|
|
368
|
-
console.log(chalk.bold.yellow(' ⚠ Running Bizar instances detected:'));
|
|
369
|
-
for (const l of lines) console.log(chalk.yellow(l));
|
|
370
|
-
console.log('');
|
|
371
|
-
console.log(chalk.yellow(' These must be stopped before npm can replace the on-disk files.'));
|
|
372
|
-
console.log(chalk.yellow(' Killing them will close any open web tabs / TUI sessions.'));
|
|
373
|
-
console.log('');
|
|
374
|
-
// Best-effort interactive confirmation. Non-TTY (CI / piped input) skips the
|
|
375
|
-
// prompt and aborts — callers should pass `--yes` explicitly in that case.
|
|
376
|
-
if (!process.stdin.isTTY) {
|
|
377
|
-
console.log(chalk.red(' Non-interactive shell detected; rerun with --yes to confirm the kill, or stop the processes manually.'));
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
try {
|
|
381
|
-
const inquirer = await import('inquirer');
|
|
382
|
-
const { ok } = await inquirer.default.prompt([
|
|
383
|
-
{
|
|
384
|
-
type: 'confirm',
|
|
385
|
-
name: 'ok',
|
|
386
|
-
message: 'Kill the running instance(s) and continue?',
|
|
387
|
-
default: true,
|
|
388
|
-
},
|
|
389
|
-
]);
|
|
390
|
-
return Boolean(ok);
|
|
391
|
-
} catch (err) {
|
|
392
|
-
console.log(chalk.red(` ! inquirer unavailable: ${err.message}`));
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function killInstances(instances) {
|
|
398
|
-
const out = [];
|
|
399
|
-
if (instances.service) {
|
|
400
|
-
const ok = await killAndWait(instances.service.pid, { label: 'bizar service' });
|
|
401
|
-
out.push({ name: 'service', ok });
|
|
402
|
-
if (ok) {
|
|
403
|
-
try { rmSync(SERVICE_PID_FILE, { force: true }); } catch { /* ignore */ }
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
if (instances.dashboard) {
|
|
407
|
-
const ok = await killAndWait(instances.dashboard.pid, { label: 'bizar-dash' });
|
|
408
|
-
out.push({ name: 'dashboard', ok });
|
|
409
|
-
if (ok) {
|
|
410
|
-
try { rmSync(DASHBOARD_PID_FILE, { force: true }); } catch { /* ignore */ }
|
|
411
|
-
try { rmSync(DASHBOARD_PORT_FILE, { force: true }); } catch { /* ignore */ }
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return out;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
// Restart the dashboard after the update completes
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Spawn a fresh dashboard process detached, returning the new PID. The
|
|
423
|
-
* dashboard will use the just-updated @polderlabs/bizar-dash code from
|
|
424
|
-
* the global npm install.
|
|
425
|
-
*/
|
|
426
|
-
function spawnFreshDashboard({ port } = {}) {
|
|
427
|
-
let globalRoot;
|
|
428
|
-
try {
|
|
429
|
-
globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
430
|
-
.toString()
|
|
431
|
-
.trim();
|
|
432
|
-
} catch {
|
|
433
|
-
return { ok: false, message: 'could not locate npm global root' };
|
|
434
|
-
}
|
|
435
|
-
// v4.4.5 — Dashboard ships inside @polderlabs/bizar (no separate
|
|
436
|
-
// @polderlabs/bizar-dash). Resolve the main package's cli.mjs.
|
|
437
|
-
const dashBin = join(globalRoot, '@polderlabs', 'bizar', 'cli', 'bin.mjs');
|
|
438
|
-
if (!existsSync(dashBin)) {
|
|
439
|
-
return {
|
|
440
|
-
ok: false,
|
|
441
|
-
message: `dashboard binary not found at ${dashBin} (was @polderlabs/bizar installed?)`,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
try {
|
|
445
|
-
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
446
|
-
} catch { /* ignore */ }
|
|
447
|
-
const args = [dashBin, 'start', '--bg'];
|
|
448
|
-
if (port) args.push(`--port=${port}`);
|
|
449
|
-
try {
|
|
450
|
-
const child = spawn(process.execPath, args, {
|
|
451
|
-
detached: true,
|
|
452
|
-
stdio: 'ignore',
|
|
453
|
-
env: { ...process.env, BIZAR_AUTO_RESPAWN: '1' },
|
|
454
|
-
});
|
|
455
|
-
child.on('error', () => { /* ignore */ });
|
|
456
|
-
child.unref();
|
|
457
|
-
return { ok: true, message: `dashboard re-spawned (pid ${child.pid})` };
|
|
458
|
-
} catch (err) {
|
|
459
|
-
return { ok: false, message: `dashboard re-spawn failed: ${err.message}` };
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// ---------------------------------------------------------------------------
|
|
464
|
-
// Repo-level update helpers
|
|
465
|
-
// ---------------------------------------------------------------------------
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* v4.4.2 — Detect whether `bizar update` is running from a git checkout
|
|
469
|
-
* or from a globally-installed npm package. When run from the npm
|
|
470
|
-
* install (REPO_ROOT has no `.git/`), the git-pull step is meaningless
|
|
471
|
-
* and aborts the entire update. We no-op it and let the `npm update -g`
|
|
472
|
-
* path handle the version bump.
|
|
473
|
-
*
|
|
474
|
-
* Returns `{ inRepo: boolean, repoRoot: string }` so the caller can
|
|
475
|
-
* decide what to do.
|
|
476
|
-
*/
|
|
477
|
-
function detectRepoMode() {
|
|
478
|
-
const gitDir = join(REPO_ROOT, '.git');
|
|
479
|
-
return {
|
|
480
|
-
inRepo: existsSync(gitDir),
|
|
481
|
-
repoRoot: REPO_ROOT,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Pull the latest changes from the git origin. Exits the process if the
|
|
487
|
-
* pull fails (merge conflict etc.), matching the task flow — an update
|
|
488
|
-
* should not proceed when the working tree is dirty.
|
|
489
|
-
*/
|
|
490
|
-
function runGitPull({ dryRun = false } = {}) {
|
|
491
|
-
const { inRepo } = detectRepoMode();
|
|
492
|
-
if (!inRepo) {
|
|
493
|
-
// Running from a global npm install — there's no checkout to pull.
|
|
494
|
-
// The npm package update is handled separately by updatePackage().
|
|
495
|
-
if (dryRun) {
|
|
496
|
-
console.log(chalk.dim(' [dry-run] skipping git pull (not a git checkout)'));
|
|
497
|
-
}
|
|
498
|
-
return { ok: true, message: 'skipped git pull (not a git checkout)' };
|
|
499
|
-
}
|
|
500
|
-
if (dryRun) {
|
|
501
|
-
console.log(chalk.dim(' [dry-run] would run: git pull --rebase'));
|
|
502
|
-
return { ok: true, message: '[dry-run] git pull --rebase' };
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
execSync('git pull --rebase', { stdio: 'inherit', cwd: REPO_ROOT, timeout: 60000 });
|
|
506
|
-
return { ok: true, message: 'git pull --rebase succeeded' };
|
|
507
|
-
} catch {
|
|
508
|
-
return { ok: false, message: 'git pull --rebase failed — resolve conflicts manually' };
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Copy bundled skills from config/skills/ to the user's .opencode/skills/
|
|
514
|
-
* directory. Currently installs: obsidian, glyph, read-the-damn-docs.
|
|
515
|
-
*/
|
|
516
|
-
async function installSkills({ dryRun = false } = {}) {
|
|
517
|
-
const skills = ['obsidian', 'glyph', 'read-the-damn-docs'];
|
|
518
|
-
const results = [];
|
|
519
|
-
for (const skill of skills) {
|
|
520
|
-
const src = join(REPO_ROOT, 'config', 'skills', skill);
|
|
521
|
-
const dst = join(homedir(), '.opencode', 'skills', skill);
|
|
522
|
-
if (dryRun) {
|
|
523
|
-
if (existsSync(src)) {
|
|
524
|
-
console.log(chalk.dim(` [dry-run] would install skill: ${skill}`));
|
|
525
|
-
}
|
|
526
|
-
} else if (existsSync(src)) {
|
|
527
|
-
try {
|
|
528
|
-
await cp(src, dst, { recursive: true });
|
|
529
|
-
console.log(chalk.green(` ✓ Installed skill: ${skill}`));
|
|
530
|
-
results.push({ skill, ok: true });
|
|
531
|
-
} catch (err) {
|
|
532
|
-
console.log(chalk.yellow(` ⚠ Failed to install skill: ${skill} — ${err.message}`));
|
|
533
|
-
results.push({ skill, ok: false });
|
|
534
|
-
}
|
|
535
|
-
} else {
|
|
536
|
-
console.log(chalk.dim(` — Source not found: config/skills/${skill} (skipping)`));
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
return results;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// ---------------------------------------------------------------------------
|
|
543
|
-
// Prompt helpers
|
|
544
|
-
// ---------------------------------------------------------------------------
|
|
545
|
-
|
|
546
|
-
async function promptForUpdates(forceAll) {
|
|
547
|
-
if (forceAll) return new Set(COMPONENTS);
|
|
548
|
-
const inquirer = await import('inquirer');
|
|
549
|
-
const { selections } = await inquirer.default.prompt([
|
|
550
|
-
{
|
|
551
|
-
type: 'checkbox',
|
|
552
|
-
name: 'selections',
|
|
553
|
-
message: 'Which components do you want to update?',
|
|
554
|
-
choices: [
|
|
555
|
-
{ name: 'opencode (the opencode CLI itself)', value: 'opencode', checked: true },
|
|
556
|
-
{ name: `bizar (${PKG_MAIN}) — includes plugin + dashboard`, value: 'bizar', checked: true },
|
|
557
|
-
],
|
|
558
|
-
},
|
|
559
|
-
]);
|
|
560
|
-
return new Set(selections);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// ---------------------------------------------------------------------------
|
|
564
|
-
// Main entry point
|
|
565
|
-
// ---------------------------------------------------------------------------
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Default behavior policy for `bizar update`:
|
|
569
|
-
* - Update EVERYTHING (opencode + bizar + dash + plugin) automatically.
|
|
570
|
-
* - Auto-install any missing component without asking.
|
|
571
|
-
* - Kill running instances without confirmation (with a brief notice).
|
|
572
|
-
* - Re-run the install script to refresh the on-disk plugin.
|
|
573
|
-
* - Restart the dashboard if it was running.
|
|
574
|
-
*
|
|
575
|
-
* The `--pick` / `-p` flag opts into the legacy per-component picker
|
|
576
|
-
* (checkbox UI) for users who want to update only some components.
|
|
577
|
-
*
|
|
578
|
-
* The `--dry-run` flag previews what would happen without touching
|
|
579
|
-
* anything.
|
|
580
|
-
*
|
|
581
|
-
* The `--no-restart` flag skips the dashboard restart step.
|
|
582
|
-
*/
|
|
583
|
-
export async function runUpdate(subargs = []) {
|
|
584
|
-
console.log(chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE ᚦ\n'));
|
|
585
|
-
|
|
586
|
-
// Parse flags
|
|
587
|
-
const assumeYes = subargs.includes('--yes') || subargs.includes('-y') || subargs.includes('--force');
|
|
588
|
-
const restartAfter = !subargs.includes('--no-restart');
|
|
589
|
-
const forceAll = subargs.includes('--all');
|
|
590
|
-
const dryRun = subargs.includes('--dry-run');
|
|
591
|
-
// Opt-in interactive picker. Without `--pick`, we update everything
|
|
592
|
-
// automatically. This matches what the user actually wants 95% of
|
|
593
|
-
// the time ("update my install") and removes a prompt that
|
|
594
|
-
// interrupted the flow.
|
|
595
|
-
const interactivePick = subargs.includes('--pick') || subargs.includes('-p');
|
|
596
|
-
|
|
597
|
-
if (dryRun) {
|
|
598
|
-
console.log(
|
|
599
|
-
chalk.dim(' --dry-run set: no installs, kills, or restarts will be performed.\n'),
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// 1. Detect running instances BEFORE doing anything else.
|
|
604
|
-
const instances = detectInstances();
|
|
605
|
-
const runningCount = (instances.service ? 1 : 0) + (instances.dashboard ? 1 : 0);
|
|
606
|
-
|
|
607
|
-
if (runningCount > 0) {
|
|
608
|
-
if (dryRun) {
|
|
609
|
-
const labels = [];
|
|
610
|
-
if (instances.service) labels.push(instances.service.label);
|
|
611
|
-
if (instances.dashboard) labels.push(instances.dashboard.label);
|
|
612
|
-
console.log(
|
|
613
|
-
chalk.dim(` [dry-run] would stop running instances: ${labels.join(', ')}`),
|
|
614
|
-
);
|
|
615
|
-
} else {
|
|
616
|
-
// In automatic mode we still print what we're about to kill
|
|
617
|
-
// but don't prompt — the operator asked for a full update and
|
|
618
|
-
// these processes would block the npm replace anyway.
|
|
619
|
-
const labels = [];
|
|
620
|
-
if (instances.service) labels.push(instances.service.label);
|
|
621
|
-
if (instances.dashboard) labels.push(instances.dashboard.label);
|
|
622
|
-
console.log(chalk.yellow(` ⚠ Stopping running instances: ${labels.join(', ')}`));
|
|
623
|
-
console.log(
|
|
624
|
-
chalk.dim(' (use `--pick` to be prompted before killing in future)'),
|
|
625
|
-
);
|
|
626
|
-
const kills = await killInstances(instances);
|
|
627
|
-
for (const k of kills) {
|
|
628
|
-
const marker = k.ok ? chalk.green('✓') : chalk.red('✗');
|
|
629
|
-
console.log(` ${marker} ${k.name} stopped`);
|
|
630
|
-
}
|
|
631
|
-
// Give the kernel a moment to release any open file handles on the
|
|
632
|
-
// npm-global directory before npm tries to replace files.
|
|
633
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
634
|
-
}
|
|
635
|
-
} else {
|
|
636
|
-
console.log(chalk.dim(' No running Bizar instances detected.'));
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// ── Git pull ──────────────────────────────────────────────────────────
|
|
640
|
-
const { inRepo } = detectRepoMode();
|
|
641
|
-
if (dryRun) {
|
|
642
|
-
if (inRepo) {
|
|
643
|
-
console.log(chalk.dim('\n [dry-run] would pull latest from origin (git pull --rebase)'));
|
|
644
|
-
} else {
|
|
645
|
-
console.log(chalk.dim('\n [dry-run] skipping git pull (not a git checkout — npm install -g handles version bump)'));
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
if (inRepo) {
|
|
649
|
-
console.log(chalk.dim('\n Pulling latest from origin...'));
|
|
650
|
-
} else {
|
|
651
|
-
console.log(chalk.dim('\n Skipping git pull (not a git checkout — using npm install -g for version bump)'));
|
|
652
|
-
}
|
|
653
|
-
const pull = runGitPull();
|
|
654
|
-
if (pull.ok) {
|
|
655
|
-
console.log(chalk.green(` ✓ ${pull.message}`));
|
|
656
|
-
} else {
|
|
657
|
-
console.log(chalk.red(` ✗ ${pull.message}`));
|
|
658
|
-
console.log(chalk.yellow(' Update aborted — resolve git conflicts and retry.'));
|
|
659
|
-
process.exit(1);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
console.log('');
|
|
663
|
-
|
|
664
|
-
// 2. Show installed vs. latest versions.
|
|
665
|
-
const cur = {
|
|
666
|
-
opencode: currentVersion('opencode-ai'),
|
|
667
|
-
bizar: currentVersion(PKG_MAIN),
|
|
668
|
-
};
|
|
669
|
-
const latest = {
|
|
670
|
-
opencode: latestVersion('opencode-ai'),
|
|
671
|
-
bizar: latestVersion(PKG_MAIN),
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
console.log('');
|
|
675
|
-
console.log(' Installed vs. latest:');
|
|
676
|
-
for (const k of COMPONENTS) {
|
|
677
|
-
const label = k === 'bizar' ? PKG_MAIN : 'opencode-ai';
|
|
678
|
-
const c = cur[k] ?? '(not installed)';
|
|
679
|
-
const l = latest[k] ?? '(unknown)';
|
|
680
|
-
const same = c === l;
|
|
681
|
-
console.log(` ${label.padEnd(28)} ${c.padEnd(15)} → ${l}${same ? ' ✓ up to date' : ' ⤵ update available'}`);
|
|
682
|
-
}
|
|
683
|
-
console.log('');
|
|
684
|
-
|
|
685
|
-
// ── Heads-up gate ───────────────────────────────────────────────────────
|
|
686
|
-
// Check .bizar/PRE_PUSH_NOTES.md for active blockers or warnings before
|
|
687
|
-
// allowing the update to proceed. Blocker entries require --force.
|
|
688
|
-
const bizarDir = findBizarDir(process.cwd());
|
|
689
|
-
if (bizarDir) {
|
|
690
|
-
const headsUp = await checkHeadsUps(bizarDir);
|
|
691
|
-
if (!headsUp.ok) {
|
|
692
|
-
if (dryRun) {
|
|
693
|
-
console.log(chalk.yellow(' Dry-run: found active blocker(s) — update would be blocked.'));
|
|
694
|
-
} else if (assumeYes || subargs.includes('--force')) {
|
|
695
|
-
console.log(chalk.yellow(' ⚠ Active blocker(s) present — proceeding due to --force.'));
|
|
696
|
-
} else {
|
|
697
|
-
console.error(chalk.red(' ✗ Active blocker(s) found in .bizar/PRE_PUSH_NOTES.md.'));
|
|
698
|
-
console.error(chalk.dim(' Archive them with `bizar heads-up archive` or'));
|
|
699
|
-
console.error(chalk.dim(' override with `--force`.'));
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
} else if (headsUp.warningCount > 0) {
|
|
703
|
-
console.log(chalk.yellow(` ⚠ ${headsUp.warningCount} warning(s) in active heads-ups.`));
|
|
704
|
-
// In automatic mode, print the warning count and continue.
|
|
705
|
-
// In interactive mode, ask for confirmation.
|
|
706
|
-
if (!assumeYes && !forceAll && process.stdin.isTTY) {
|
|
707
|
-
try {
|
|
708
|
-
const inquirer = await import('inquirer');
|
|
709
|
-
const { proceed } = await inquirer.default.prompt([
|
|
710
|
-
{
|
|
711
|
-
type: 'confirm',
|
|
712
|
-
name: 'proceed',
|
|
713
|
-
message: 'Heads-up warnings exist. Continue with update?',
|
|
714
|
-
default: true,
|
|
715
|
-
},
|
|
716
|
-
]);
|
|
717
|
-
if (!proceed) {
|
|
718
|
-
console.log(chalk.yellow(' Update cancelled by user.'));
|
|
719
|
-
process.exit(0);
|
|
720
|
-
}
|
|
721
|
-
} catch {
|
|
722
|
-
// inquirer unavailable — continue anyway
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
} else {
|
|
726
|
-
console.log(chalk.green(' ✓ Heads-ups: clear'));
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
console.log('');
|
|
730
|
-
|
|
731
|
-
// 3. Decide what to update. Default = everything. Any missing package
|
|
732
|
-
// is automatically included so a partial install gets completed.
|
|
733
|
-
let selected;
|
|
734
|
-
if (interactivePick && !dryRun && !forceAll && !assumeYes) {
|
|
735
|
-
selected = await promptForUpdates(false);
|
|
736
|
-
} else {
|
|
737
|
-
// Auto-select: all components + any missing one (so a broken install
|
|
738
|
-
// gets repaired automatically).
|
|
739
|
-
selected = new Set(COMPONENTS);
|
|
740
|
-
for (const k of COMPONENTS) {
|
|
741
|
-
if (cur[k] === null && latest[k] !== null) selected.add(k);
|
|
742
|
-
}
|
|
743
|
-
if (interactivePick) {
|
|
744
|
-
// --pick + --dry-run / --all / --yes → still update everything
|
|
745
|
-
// but make the auto-selection visible so the dry-run output is
|
|
746
|
-
// informative.
|
|
747
|
-
console.log(chalk.dim(` Auto-selecting all components (--pick ignored due to flags).`));
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (selected.size === 0) {
|
|
752
|
-
console.log(chalk.dim(' Nothing to update.'));
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
console.log(chalk.cyan(` Updating: ${[...selected].join(', ')}\n`));
|
|
757
|
-
|
|
758
|
-
const results = [];
|
|
759
|
-
if (selected.has('opencode')) {
|
|
760
|
-
console.log(chalk.bold(' → opencode'));
|
|
761
|
-
results.push(['opencode', updateOpencode({ dryRun })]);
|
|
762
|
-
}
|
|
763
|
-
if (selected.has('bizar')) {
|
|
764
|
-
console.log(chalk.bold(` → ${PKG_MAIN}`));
|
|
765
|
-
results.push(['bizar', updatePackage(PKG_MAIN, { dryRun })]);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// 4. Re-run the install script if anything relevant changed.
|
|
769
|
-
const anySuccess = results.some(([, r]) => r.ok);
|
|
770
|
-
if (anySuccess && selected.has('bizar')) {
|
|
771
|
-
console.log('');
|
|
772
|
-
if (dryRun) {
|
|
773
|
-
console.log(
|
|
774
|
-
chalk.dim(
|
|
775
|
-
' [dry-run] would re-run install.sh to refresh agent files + plugin copy',
|
|
776
|
-
),
|
|
777
|
-
);
|
|
778
|
-
} else {
|
|
779
|
-
const rerun = rerunInstallScript();
|
|
780
|
-
if (rerun.ok) {
|
|
781
|
-
console.log(chalk.green(`\n ✓ ${rerun.message}`));
|
|
782
|
-
} else {
|
|
783
|
-
console.log(chalk.yellow(`\n ⚠ ${rerun.message}`));
|
|
784
|
-
console.log(chalk.dim(' Run `bizar install` to refresh agent files + plugin copy.'));
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ── Install bundled skills ──────────────────────────────────────────
|
|
790
|
-
console.log(chalk.bold('\n → Installing bundled skills...'));
|
|
791
|
-
const skillResults = await installSkills({ dryRun });
|
|
792
|
-
const skillOk = skillResults.length === 0 || skillResults.every((r) => r.ok);
|
|
793
|
-
|
|
794
|
-
// v4.4.5 — Dashboard dist ships prebuilt inside the npm package. No
|
|
795
|
-
// rebuild step is needed (and we can't run `vite build` from the
|
|
796
|
-
// installed package anyway — node_modules/vite lives outside the
|
|
797
|
-
// plugin's runtime context). The next launch picks up the new dist
|
|
798
|
-
// when `bizar dash start --bg` runs.
|
|
799
|
-
|
|
800
|
-
// ── Restart dashboard ─────────────────────────────────────────────
|
|
801
|
-
// Restart if it was running before.
|
|
802
|
-
if (restartAfter && instances.dashboard) {
|
|
803
|
-
if (dryRun) {
|
|
804
|
-
console.log('');
|
|
805
|
-
console.log(chalk.dim(' [dry-run] would restart dashboard with the new code'));
|
|
806
|
-
} else {
|
|
807
|
-
console.log('');
|
|
808
|
-
console.log(chalk.cyan(' Restarting dashboard with the new code...'));
|
|
809
|
-
const res = spawnFreshDashboard({ port: instances.dashboard?.port || undefined });
|
|
810
|
-
if (res.ok) {
|
|
811
|
-
console.log(chalk.green(` ✓ ${res.message}`));
|
|
812
|
-
} else {
|
|
813
|
-
console.log(chalk.yellow(` ⚠ ${res.message}`));
|
|
814
|
-
console.log(chalk.dim(' Start it manually with `bizar dash start --bg`.'));
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// 6. Summary
|
|
820
|
-
console.log('');
|
|
821
|
-
console.log(' Summary:');
|
|
822
|
-
for (const [name, r] of results) {
|
|
823
|
-
const marker = r.ok ? chalk.green('✓') : chalk.red('✗');
|
|
824
|
-
console.log(` ${marker} ${name.padEnd(10)} ${r.message}`);
|
|
825
|
-
}
|
|
826
|
-
// Skills
|
|
827
|
-
for (const sr of skillResults) {
|
|
828
|
-
const marker = sr.ok ? chalk.green('✓') : chalk.red('✗');
|
|
829
|
-
console.log(` ${marker} ${'skill/skills'.padEnd(10)} ${sr.ok ? 'installed' : `failed: ${sr.skill}`}`);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
const allResults = [
|
|
833
|
-
...results.map(([, r]) => r),
|
|
834
|
-
...skillResults,
|
|
835
|
-
];
|
|
836
|
-
const anyFail = allResults.some((r) => !r.ok);
|
|
837
|
-
if (anyFail) {
|
|
838
|
-
console.log(chalk.yellow('\n Some steps had issues. See messages above.'));
|
|
839
|
-
if (allResults.filter((r) => !r.ok).some((r) => r.message && r.message.includes('failed'))) {
|
|
840
|
-
process.exit(1);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
if (dryRun) {
|
|
844
|
-
console.log(
|
|
845
|
-
chalk.green('\n ✓ Dry-run complete (no installs, kills, or restarts performed)\n'),
|
|
846
|
-
);
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
console.log(chalk.green('\n ✓ Update complete\n'));
|
|
850
|
-
|
|
851
|
-
// 7. Post-update health check (v3.12.2). Catches a bad config merge or
|
|
852
|
-
// missing files before the user discovers it via a broken opencode session.
|
|
853
|
-
try {
|
|
854
|
-
const { runDoctor } = await import('./doctor.mjs');
|
|
855
|
-
const result = await runDoctor({ silent: true });
|
|
856
|
-
if (result.failed > 0) {
|
|
857
|
-
console.log('');
|
|
858
|
-
console.log(
|
|
859
|
-
chalk.yellow(' ⚠ Post-update health check found issues:'),
|
|
860
|
-
);
|
|
861
|
-
for (const r of result.results) {
|
|
862
|
-
if (!r.ok) {
|
|
863
|
-
console.log(chalk.red(` ✗ ${r.name}: ${r.message}`));
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
console.log(chalk.dim(' Run `bizar doctor` for details.'));
|
|
867
|
-
process.exit(1);
|
|
868
|
-
}
|
|
869
|
-
} catch (err) {
|
|
870
|
-
// Doctor import or runtime failure shouldn't crash the update —
|
|
871
|
-
// log a hint and let the user run `bizar doctor` themselves.
|
|
872
|
-
console.log(
|
|
873
|
-
chalk.yellow(
|
|
874
|
-
` ⚠ Post-update health check could not run: ${err.message}`,
|
|
875
|
-
),
|
|
876
|
-
);
|
|
877
|
-
console.log(chalk.dim(' Run `bizar doctor` manually to verify the install.'));
|
|
878
|
-
}
|
|
879
|
-
}
|
|
2
|
+
* update.mjs — `bizar update` subcommand (v4.4.7 — thin wrapper).
|
|
3
|
+
*
|
|
4
|
+
* The full update logic now lives in `cli/provision.mjs:runProvision`.
|
|
5
|
+
* `bizar install` and `bizar update` are the same code path with
|
|
6
|
+
* different `mode` flags. This file just re-exports the API the rest of
|
|
7
|
+
* the codebase expects.
|
|
8
|
+
*
|
|
9
|
+
* Why a thin wrapper:
|
|
10
|
+
* - One source of truth. Install + update used to drift apart (and
|
|
11
|
+
* did — install.mjs and update.mjs each grew their own copy of
|
|
12
|
+
* "copy plugin, patch opencode.json, sync skills, run doctor").
|
|
13
|
+
* Now both call the same function.
|
|
14
|
+
* - Easier to maintain. Adding a new step means editing provision.mjs
|
|
15
|
+
* once; both `install` and `update` pick it up.
|
|
16
|
+
* - Same idempotency guarantees. Both modes probe existing state
|
|
17
|
+
* first and skip work that's already done.
|
|
18
|
+
*
|
|
19
|
+
* Backward-compatible: `runUpdate` is still exported with the same
|
|
20
|
+
* signature (`runUpdate(subargs: string[])`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
runUpdate,
|
|
25
|
+
runProvision,
|
|
26
|
+
detectState,
|
|
27
|
+
readLivePid,
|
|
28
|
+
killAndWait,
|
|
29
|
+
} from './provision.mjs';
|