@polderlabs/bizar 4.4.9 → 4.4.11
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/src/server/mods-loader.mjs +169 -1
- package/cli/bin.mjs +39 -6
- package/cli/provision.mjs +182 -2
- package/config/skills/glyph/SKILL.md +74 -58
- package/package.json +1 -1
|
@@ -178,6 +178,138 @@ export const modsLoader = {
|
|
|
178
178
|
mkdirSync(MODS_DIR, { recursive: true });
|
|
179
179
|
},
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* v4.4.11 — Validate an installed mod without mounting it.
|
|
183
|
+
*
|
|
184
|
+
* Runs after a copy (in `installFromPath` + `installFromRegistry`) and
|
|
185
|
+
* before the install is reported as successful. Returns a structured
|
|
186
|
+
* `{ ok, errors, warnings }` so callers (the API + the CLI + the
|
|
187
|
+
* provisioner) can surface failures consistently.
|
|
188
|
+
*
|
|
189
|
+
* Checks performed:
|
|
190
|
+
* 1. `mod.json` is valid JSON with required fields (id, name, version).
|
|
191
|
+
* 2. `entry.route` (if declared) is a file that exists and is readable.
|
|
192
|
+
* We pre-parse it with `node --check` to catch syntax errors.
|
|
193
|
+
* 3. Permissions declared in mod.json are validated against the
|
|
194
|
+
* allowed list (delegates to mod-security.mjs).
|
|
195
|
+
* 4. If `entry.validate` is declared, we dynamic-import that file
|
|
196
|
+
* and call its `default` or named `validate` export. The hook can
|
|
197
|
+
* throw to fail the install.
|
|
198
|
+
*
|
|
199
|
+
* Never throws — always returns a structured result.
|
|
200
|
+
*/
|
|
201
|
+
async validateModInstallation(id) {
|
|
202
|
+
const errors = [];
|
|
203
|
+
const warnings = [];
|
|
204
|
+
const dir = join(MODS_DIR, id);
|
|
205
|
+
if (!existsSync(dir)) {
|
|
206
|
+
return { ok: false, errors: [`mod directory not found: ${dir}`], warnings };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 1. mod.json valid + required fields
|
|
210
|
+
const manifest = safeReadJSON(join(dir, 'mod.json'), null);
|
|
211
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
errors: ['mod.json is missing or invalid JSON'],
|
|
215
|
+
warnings,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
for (const field of ['id', 'name', 'version']) {
|
|
219
|
+
if (!manifest[field] || typeof manifest[field] !== 'string') {
|
|
220
|
+
errors.push(`mod.json is missing required field "${field}"`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (manifest.id && manifest.id !== id) {
|
|
224
|
+
errors.push(`mod.json "id" field ("${manifest.id}") does not match the directory name ("${id}")`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. entry.route exists + parses
|
|
228
|
+
const entry = manifest.entry || {};
|
|
229
|
+
if (entry.route) {
|
|
230
|
+
const routePath = join(dir, entry.route);
|
|
231
|
+
if (!existsSync(routePath)) {
|
|
232
|
+
errors.push(`entry.route "${entry.route}" does not exist at ${routePath}`);
|
|
233
|
+
} else {
|
|
234
|
+
// Pre-parse the route.mjs with `node --check` to catch syntax
|
|
235
|
+
// errors without executing it. Skipped if node is not available.
|
|
236
|
+
try {
|
|
237
|
+
const { spawnSync } = await import('node:child_process');
|
|
238
|
+
const probe = spawnSync(process.execPath, ['--check', routePath], {
|
|
239
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
240
|
+
timeout: 5000,
|
|
241
|
+
});
|
|
242
|
+
if (probe.status !== 0) {
|
|
243
|
+
// Extract the first error line from the syntax checker
|
|
244
|
+
// output. `node --check` prints something like:
|
|
245
|
+
// /path/to/route.mjs:5
|
|
246
|
+
// syntax is wrong here
|
|
247
|
+
// ^^^
|
|
248
|
+
// SyntaxError: message
|
|
249
|
+
// We want the last "SyntaxError:" line.
|
|
250
|
+
const stderr = (probe.stderr || '').toString();
|
|
251
|
+
const lines = stderr.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
252
|
+
const errLine = lines.reverse().find((l) => l.toLowerCase().includes('syntaxerror')) || lines[0] || 'unknown';
|
|
253
|
+
errors.push(`entry.route "${entry.route}" has a syntax error: ${errLine}`);
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// node --check unavailable — skip (not fatal)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 3. Permissions valid
|
|
262
|
+
if (Array.isArray(manifest.permissions)) {
|
|
263
|
+
const { invalid } = parsePermissions(manifest.permissions);
|
|
264
|
+
for (const p of invalid) {
|
|
265
|
+
warnings.push(`mod declares unknown permission: ${p}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 4. Optional entry.validate hook
|
|
270
|
+
if (entry.validate) {
|
|
271
|
+
const validatePath = join(dir, entry.validate);
|
|
272
|
+
if (!existsSync(validatePath)) {
|
|
273
|
+
errors.push(`entry.validate "${entry.validate}" does not exist at ${validatePath}`);
|
|
274
|
+
} else {
|
|
275
|
+
try {
|
|
276
|
+
// Dynamic import — the validate hook is opt-in. The mod may
|
|
277
|
+
// export `default` (function) or named export `validate`.
|
|
278
|
+
const mod = await import(/* @vite-ignore */ `file://${validatePath}`);
|
|
279
|
+
const fn = mod.default || mod.validate;
|
|
280
|
+
if (typeof fn !== 'function') {
|
|
281
|
+
warnings.push(
|
|
282
|
+
`entry.validate "${entry.validate}" does not export a function (got ${typeof fn})`,
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
// Run the validate hook with a 5s timeout. The hook is
|
|
286
|
+
// trusted (it's part of the mod) but we don't want a bad
|
|
287
|
+
// hook to hang the install.
|
|
288
|
+
const result = await Promise.race([
|
|
289
|
+
Promise.resolve()
|
|
290
|
+
.then(() => fn({ mod: manifest, dir }))
|
|
291
|
+
.catch((err) => ({ ok: false, error: err.message })),
|
|
292
|
+
new Promise((resolve) =>
|
|
293
|
+
setTimeout(() => resolve({ ok: false, error: 'validate hook timed out after 5s' }), 5000),
|
|
294
|
+
),
|
|
295
|
+
]);
|
|
296
|
+
if (result && result.ok === false) {
|
|
297
|
+
errors.push(`mod validate hook failed: ${result.error || 'unknown'}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
errors.push(`failed to load entry.validate "${entry.validate}": ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
ok: errors.length === 0,
|
|
308
|
+
errors,
|
|
309
|
+
warnings,
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
|
|
181
313
|
/** List all installed mods. v3.3.1 — never throws. */
|
|
182
314
|
list() {
|
|
183
315
|
try {
|
|
@@ -211,7 +343,7 @@ export const modsLoader = {
|
|
|
211
343
|
* Install a mod from a local path. Copies the folder into
|
|
212
344
|
* `~/.config/bizar/mods/<id>/`. The id is the source folder's basename.
|
|
213
345
|
*/
|
|
214
|
-
installFromPath(sourcePath) {
|
|
346
|
+
async installFromPath(sourcePath) {
|
|
215
347
|
if (!existsSync(sourcePath)) {
|
|
216
348
|
throw new Error(`source path does not exist: ${sourcePath}`);
|
|
217
349
|
}
|
|
@@ -243,6 +375,24 @@ export const modsLoader = {
|
|
|
243
375
|
// (agents/, commands/, skills/). This makes the mod's rules binding
|
|
244
376
|
// on every agent at session start.
|
|
245
377
|
installModInstructions(id, target);
|
|
378
|
+
// v4.4.11 — Smoke-test the mod before reporting success. We
|
|
379
|
+
// uninstall on failure so a broken mod doesn't leave a half-
|
|
380
|
+
// copied directory the user has to clean up manually.
|
|
381
|
+
const validation = await this.validateModInstallation(id);
|
|
382
|
+
if (!validation.ok) {
|
|
383
|
+
uninstallModInstructions(id);
|
|
384
|
+
rmSync(target, { recursive: true, force: true });
|
|
385
|
+
const err = new Error(
|
|
386
|
+
`mod "${id}" failed post-install validation: ${validation.errors.join('; ')}`,
|
|
387
|
+
);
|
|
388
|
+
err.validation = validation;
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
if (validation.warnings.length > 0) {
|
|
392
|
+
console.warn(
|
|
393
|
+
`[mods-loader] mod "${id}" installed with warnings: ${validation.warnings.join('; ')}`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
246
396
|
return loadMod({ id, dir: target });
|
|
247
397
|
},
|
|
248
398
|
|
|
@@ -494,6 +644,24 @@ export const modsLoader = {
|
|
|
494
644
|
// v3.20 — install mod instructions into the user's opencode config
|
|
495
645
|
// (agents/, commands/, skills/). Triggered on registry install too.
|
|
496
646
|
installModInstructions(id, target);
|
|
647
|
+
// v4.4.11 — Smoke-test the mod before reporting success. We
|
|
648
|
+
// uninstall on failure so a broken mod doesn't leave a half-
|
|
649
|
+
// copied directory the user has to clean up manually.
|
|
650
|
+
const validation = await this.validateModInstallation(id);
|
|
651
|
+
if (!validation.ok) {
|
|
652
|
+
uninstallModInstructions(id);
|
|
653
|
+
rmSync(target, { recursive: true, force: true });
|
|
654
|
+
const err = new Error(
|
|
655
|
+
`mod "${id}" failed post-install validation: ${validation.errors.join('; ')}`,
|
|
656
|
+
);
|
|
657
|
+
err.validation = validation;
|
|
658
|
+
throw err;
|
|
659
|
+
}
|
|
660
|
+
if (validation.warnings.length > 0) {
|
|
661
|
+
console.warn(
|
|
662
|
+
`[mods-loader] mod "${id}" installed with warnings: ${validation.warnings.join('; ')}`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
497
665
|
return loadMod({ id, dir: target });
|
|
498
666
|
},
|
|
499
667
|
|
package/cli/bin.mjs
CHANGED
|
@@ -156,10 +156,11 @@ function showInstallHelp() {
|
|
|
156
156
|
bizar install — Run the unified BizarHarness installer
|
|
157
157
|
|
|
158
158
|
Usage:
|
|
159
|
-
bizar install
|
|
160
|
-
bizar install --dry-run
|
|
161
|
-
bizar install --force
|
|
162
|
-
bizar install --
|
|
159
|
+
bizar install Install (or refresh) every component
|
|
160
|
+
bizar install --dry-run Print what would happen, change nothing
|
|
161
|
+
bizar install --force Overwrite existing files
|
|
162
|
+
bizar install --with-mods a,b,c Opt-in: install specific mods as part of the run
|
|
163
|
+
bizar install --help Show this help
|
|
163
164
|
|
|
164
165
|
Description:
|
|
165
166
|
v4.4.7+ — unified installer. Same code path as 'bizar update'; the
|
|
@@ -194,6 +195,7 @@ function showUpdateHelp() {
|
|
|
194
195
|
bizar update --dry-run Print what would happen, change nothing
|
|
195
196
|
bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
|
|
196
197
|
bizar update --yes Same as --force, but named for one-line scripts
|
|
198
|
+
bizar update --with-mods a,b,c Opt-in: install specific mods as part of the run
|
|
197
199
|
bizar update --help Show this help
|
|
198
200
|
|
|
199
201
|
Components updated:
|
|
@@ -580,6 +582,22 @@ function parseFlag(name) {
|
|
|
580
582
|
return args[idx + 1] || null;
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
/**
|
|
586
|
+
* v4.4.11 — Parse `--with-mods <csv>` from the given subargs slice.
|
|
587
|
+
* Returns `null` if the flag isn't present (the provisioner's
|
|
588
|
+
* "don't touch mods" default), or a string[] of mod ids if it is.
|
|
589
|
+
*/
|
|
590
|
+
function parseWithModsFlag(subargs) {
|
|
591
|
+
const idx = subargs.indexOf('--with-mods');
|
|
592
|
+
if (idx === -1) return null;
|
|
593
|
+
const raw = subargs[idx + 1];
|
|
594
|
+
if (!raw || raw.startsWith('--')) return [];
|
|
595
|
+
return raw
|
|
596
|
+
.split(',')
|
|
597
|
+
.map((s) => s.trim())
|
|
598
|
+
.filter(Boolean);
|
|
599
|
+
}
|
|
600
|
+
|
|
583
601
|
async function readAutoLaunchWeb() {
|
|
584
602
|
try {
|
|
585
603
|
const fs = await import('node:fs');
|
|
@@ -677,7 +695,19 @@ async function main() {
|
|
|
677
695
|
else await runTestGate();
|
|
678
696
|
} else if (args[0] === 'update') {
|
|
679
697
|
if (isHelpRequest) showUpdateHelp();
|
|
680
|
-
else
|
|
698
|
+
else {
|
|
699
|
+
// v4.4.11 — Same --with-mods opt-in for update.
|
|
700
|
+
const withMods = parseWithModsFlag(args.slice(1));
|
|
701
|
+
// runUpdate expects (subargs: string[], opts?: object). Splice
|
|
702
|
+
// --with-mods <csv> out of subargs since the provisioner now
|
|
703
|
+
// takes it via opts, not as a positional arg.
|
|
704
|
+
const subargs = args.slice(1).filter((a, i, arr) => {
|
|
705
|
+
if (a === '--with-mods') return false;
|
|
706
|
+
if (arr[i - 1] === '--with-mods') return false;
|
|
707
|
+
return true;
|
|
708
|
+
});
|
|
709
|
+
await runUpdate(subargs, { withMods });
|
|
710
|
+
}
|
|
681
711
|
} else if (args[0] === 'dev-link') {
|
|
682
712
|
if (isHelpRequest) showDevLinkHelp();
|
|
683
713
|
else {
|
|
@@ -741,7 +771,10 @@ async function main() {
|
|
|
741
771
|
} else if (args[0] === 'install') {
|
|
742
772
|
if (isHelpRequest) showInstallHelp();
|
|
743
773
|
else {
|
|
744
|
-
|
|
774
|
+
// v4.4.11 — Parse --with-mods <csv> to opt into mod installs
|
|
775
|
+
// during the run. Default: mods are NEVER touched.
|
|
776
|
+
const withMods = parseWithModsFlag(args.slice(1));
|
|
777
|
+
await runInstaller({ withMods });
|
|
745
778
|
// v4.4.3 — After install, repair any stale bin symlinks so the
|
|
746
779
|
// user picks up the new code (the installer itself may have
|
|
747
780
|
// been running from a stale install path).
|
package/cli/provision.mjs
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import chalk from 'chalk';
|
|
40
40
|
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
41
|
-
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
41
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
|
42
42
|
import { homedir } from 'node:os';
|
|
43
43
|
import { join, dirname } from 'node:path';
|
|
44
44
|
import { fileURLToPath } from 'node:url';
|
|
@@ -272,6 +272,13 @@ export function detectState({ cwd = process.cwd() } = {}) {
|
|
|
272
272
|
// ── Git checkout? ─────────────────────────────────────────────────
|
|
273
273
|
const gitRepo = existsSync(join(REPO_ROOT, '.git'));
|
|
274
274
|
|
|
275
|
+
// ── Installed mods ─────────────────────────────────────────────────
|
|
276
|
+
// v4.4.11 — `bizar install` and `bizar update` never install or
|
|
277
|
+
// upgrade mods. The list below is informational only. Use
|
|
278
|
+
// `bizar mod install <id>` to add a mod explicitly.
|
|
279
|
+
const MODS_DIR = join(BIZAR_HOME, 'mods');
|
|
280
|
+
const installedMods = listInstalledMods(MODS_DIR);
|
|
281
|
+
|
|
275
282
|
return {
|
|
276
283
|
pkgRoot,
|
|
277
284
|
pkgVersion,
|
|
@@ -302,9 +309,52 @@ export function detectState({ cwd = process.cwd() } = {}) {
|
|
|
302
309
|
opencodeCli,
|
|
303
310
|
headsUpState,
|
|
304
311
|
gitRepo,
|
|
312
|
+
installedMods,
|
|
305
313
|
};
|
|
306
314
|
}
|
|
307
315
|
|
|
316
|
+
/**
|
|
317
|
+
* v4.4.11 — Lightweight read-only scan of `~/.config/bizar/mods/`.
|
|
318
|
+
* Returns one entry per mod folder with the id + enabled flag parsed
|
|
319
|
+
* from mod.json. Never throws; mods with a missing or invalid mod.json
|
|
320
|
+
* are reported as `{id, error}` so the provisioner can surface them.
|
|
321
|
+
*/
|
|
322
|
+
function listInstalledMods(modsDir) {
|
|
323
|
+
const out = [];
|
|
324
|
+
if (!existsSync(modsDir)) return out;
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = readdirSync(modsDir, { withFileTypes: true });
|
|
328
|
+
} catch {
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (!entry.isDirectory()) continue;
|
|
333
|
+
const dir = join(modsDir, entry.name);
|
|
334
|
+
const manifestPath = join(dir, 'mod.json');
|
|
335
|
+
if (!existsSync(manifestPath)) {
|
|
336
|
+
out.push({ id: entry.name, error: 'missing mod.json' });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
let manifest;
|
|
340
|
+
try {
|
|
341
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
342
|
+
} catch {
|
|
343
|
+
out.push({ id: entry.name, error: 'invalid mod.json' });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
out.push({
|
|
347
|
+
id: manifest.id || entry.name,
|
|
348
|
+
name: manifest.name || entry.name,
|
|
349
|
+
version: manifest.version || '?',
|
|
350
|
+
enabled: manifest.enabled !== false,
|
|
351
|
+
installedAt: manifest.installedAt || null,
|
|
352
|
+
path: dir,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
308
358
|
/**
|
|
309
359
|
* Lightweight CommonJS-ish require for ESM contexts. Used to detect the
|
|
310
360
|
* service-controller + heads-up modules without paying the static-import
|
|
@@ -681,6 +731,125 @@ export async function runDoctor({ silent = false } = {}) {
|
|
|
681
731
|
}
|
|
682
732
|
}
|
|
683
733
|
|
|
734
|
+
/**
|
|
735
|
+
* v4.4.11 — The mods step. By default, NEVER install or upgrade mods
|
|
736
|
+
* during `bizar install` or `bizar update`. The step just reports the
|
|
737
|
+
* current mod list so the user can see what's installed.
|
|
738
|
+
*
|
|
739
|
+
* To install a mod as part of the run, the user must opt in by:
|
|
740
|
+
* - passing `--with-mods <id1,id2>` to `bizar install` / `bizar update`
|
|
741
|
+
* - or setting the env var `BIZAR_MODS_AUTO_INSTALL=allow`
|
|
742
|
+
*
|
|
743
|
+
* When opted in, we shell to the running dashboard's `/api/mods`
|
|
744
|
+
* endpoint (which already validates the mod) — we don't duplicate the
|
|
745
|
+
* install logic here.
|
|
746
|
+
*/
|
|
747
|
+
export async function runModsStep({ mode, dryRun, force, withMods, state }) {
|
|
748
|
+
// List the current mods (from the state we already detected).
|
|
749
|
+
const installed = state?.installedMods ?? [];
|
|
750
|
+
if (installed.length === 0) {
|
|
751
|
+
console.log(chalk.dim(' Mods: 0 installed.'));
|
|
752
|
+
return { ok: true, message: 'no mods installed', touched: false };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const enabled = installed.filter((m) => m.enabled).length;
|
|
756
|
+
const disabled = installed.length - enabled;
|
|
757
|
+
const summary = `${installed.length} installed (${enabled} enabled${disabled ? `, ${disabled} disabled` : ''})`;
|
|
758
|
+
|
|
759
|
+
// Resolve which mods the user asked us to install. The CLI parses
|
|
760
|
+
// `--with-mods a,b,c` into a string array; the provisioner accepts
|
|
761
|
+
// the same. We also accept a single env var as a comma-separated list.
|
|
762
|
+
const envList = (process.env.BIZAR_MODS_AUTO_INSTALL || '')
|
|
763
|
+
.split(',')
|
|
764
|
+
.map((s) => s.trim())
|
|
765
|
+
.filter(Boolean);
|
|
766
|
+
const wantedMods = Array.isArray(withMods) && withMods.length > 0
|
|
767
|
+
? withMods
|
|
768
|
+
: (process.env.BIZAR_MODS_AUTO_INSTALL === 'allow' ? [] : envList);
|
|
769
|
+
|
|
770
|
+
// Default behavior: no install. Just print the list.
|
|
771
|
+
if (!wantedMods || wantedMods.length === 0) {
|
|
772
|
+
console.log(chalk.dim(` Mods: ${summary}. Not modified (use \`bizar mod install <id>\` to add one).`));
|
|
773
|
+
// Surface mods with errors so the user knows they need attention.
|
|
774
|
+
const broken = installed.filter((m) => m.error);
|
|
775
|
+
for (const m of broken) {
|
|
776
|
+
console.log(chalk.yellow(` ⚠ ${m.id}: ${m.error}`));
|
|
777
|
+
}
|
|
778
|
+
return { ok: true, message: `${summary}, not modified`, touched: false };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Opt-in install path. Talk to the dashboard over HTTP — the
|
|
782
|
+
// dashboard's `POST /api/mods` endpoint already does the actual
|
|
783
|
+
// install + validation. If the dashboard isn't reachable, the
|
|
784
|
+
// install fails loudly.
|
|
785
|
+
console.log(chalk.cyan(` Installing ${wantedMods.length} mod(s) via dashboard API: ${wantedMods.join(', ')}`));
|
|
786
|
+
const errors = [];
|
|
787
|
+
for (const id of wantedMods) {
|
|
788
|
+
try {
|
|
789
|
+
const result = await installModViaDashboard(id, { dryRun });
|
|
790
|
+
if (result.ok) {
|
|
791
|
+
console.log(chalk.green(` ✓ ${id}: ${result.message}`));
|
|
792
|
+
} else {
|
|
793
|
+
console.log(chalk.red(` ✗ ${id}: ${result.message}`));
|
|
794
|
+
errors.push(`${id}: ${result.message}`);
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
798
|
+
console.log(chalk.red(` ✗ ${id}: ${msg}`));
|
|
799
|
+
errors.push(`${id}: ${msg}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (errors.length > 0) {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
message: `mod install failed for ${errors.length} mod(s): ${errors.join('; ')}`,
|
|
806
|
+
touched: true,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
return { ok: true, message: `${wantedMods.length} mod(s) installed`, touched: true };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* v4.4.11 — POST to the dashboard's /api/mods endpoint to install a mod.
|
|
814
|
+
* We re-use the dashboard's own validation pipeline (mod-loader.mjs
|
|
815
|
+
* runs the same manifest + route.mjs + permissions checks) so we
|
|
816
|
+
* don't have to duplicate them here.
|
|
817
|
+
*/
|
|
818
|
+
async function installModViaDashboard(id, { dryRun }) {
|
|
819
|
+
if (dryRun) {
|
|
820
|
+
return { ok: true, message: '[dry-run] would install via dashboard' };
|
|
821
|
+
}
|
|
822
|
+
// Find the dashboard's port from BIZAR_HOME/dashboard.port.
|
|
823
|
+
const portFile = join(BIZAR_HOME, 'dashboard.port');
|
|
824
|
+
let port = 4321;
|
|
825
|
+
try {
|
|
826
|
+
if (existsSync(portFile)) {
|
|
827
|
+
const parsed = parseInt(readTextSafe(portFile, '4321').trim(), 10);
|
|
828
|
+
if (Number.isFinite(parsed) && parsed > 0) port = parsed;
|
|
829
|
+
}
|
|
830
|
+
} catch { /* ignore */ }
|
|
831
|
+
// We need an auth token. The dashboard's install endpoint is under
|
|
832
|
+
// /api/* which is auth-gated. Fetch the auth status + token via
|
|
833
|
+
// /api/auth/status (skipped from auth via the skipPaths list in
|
|
834
|
+
// server.mjs). If the dashboard has auth enabled, the user needs to
|
|
835
|
+
// supply a token via BIZAR_DASHBOARD_TOKEN.
|
|
836
|
+
const headers = { 'content-type': 'application/json' };
|
|
837
|
+
const token = process.env.BIZAR_DASHBOARD_TOKEN;
|
|
838
|
+
if (token) headers['authorization'] = `Basic ${Buffer.from(`opencode:${token}`).toString('base64')}`;
|
|
839
|
+
const url = `http://127.0.0.1:${port}/api/mods`;
|
|
840
|
+
const res = await fetch(url, {
|
|
841
|
+
method: 'POST',
|
|
842
|
+
headers,
|
|
843
|
+
body: JSON.stringify({ id }),
|
|
844
|
+
});
|
|
845
|
+
if (!res.ok) {
|
|
846
|
+
const text = await res.text().catch(() => '');
|
|
847
|
+
return { ok: false, message: `dashboard returned ${res.status}: ${text.slice(0, 200) || '(no body)'}` };
|
|
848
|
+
}
|
|
849
|
+
const data = await res.json().catch(() => ({}));
|
|
850
|
+
return { ok: true, message: data?.name ? `installed ${data.name}@${data.version}` : 'installed' };
|
|
851
|
+
}
|
|
852
|
+
|
|
684
853
|
// ─── Top-level orchestration ──────────────────────────────────────────────────
|
|
685
854
|
|
|
686
855
|
/**
|
|
@@ -785,6 +954,7 @@ export async function runProvision(opts = {}) {
|
|
|
785
954
|
skipSystemDeps = false,
|
|
786
955
|
skipHeadsUps = false,
|
|
787
956
|
yes = false,
|
|
957
|
+
withMods = null, // string[] — opt-in mod install. Default null = don't touch mods.
|
|
788
958
|
} = opts;
|
|
789
959
|
|
|
790
960
|
const banner = mode === 'update'
|
|
@@ -897,7 +1067,17 @@ export async function runProvision(opts = {}) {
|
|
|
897
1067
|
stepResults.push({ label: 'dashboard-restart', ...r });
|
|
898
1068
|
}
|
|
899
1069
|
|
|
900
|
-
// ── 11.
|
|
1070
|
+
// ── 11. Mods (opt-in only) ─────────────────────────────────────────
|
|
1071
|
+
// v4.4.11 — `bizar install` and `bizar update` never install or
|
|
1072
|
+
// upgrade mods by default. The provisioner reports the current mod
|
|
1073
|
+
// list so the user can see what's installed, then exits the mod step
|
|
1074
|
+
// without touching anything. To install a mod, pass
|
|
1075
|
+
// `--with-mods <id1,id2>` or set `BIZAR_MODS_AUTO_INSTALL=allow`.
|
|
1076
|
+
console.log('');
|
|
1077
|
+
const modsStep = await runModsStep({ mode, dryRun, force, withMods, state });
|
|
1078
|
+
stepResults.push({ label: 'mods', ...modsStep });
|
|
1079
|
+
|
|
1080
|
+
// ── 12. Doctor health check ───────────────────────────────────────
|
|
901
1081
|
console.log('');
|
|
902
1082
|
const doctor = await runDoctor({ silent: true });
|
|
903
1083
|
if (doctor.failed > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: glyph
|
|
3
|
-
description: Create
|
|
4
|
-
version:
|
|
3
|
+
description: Create visual glyphs at `artifacts/<slug>/` for plans, recaps, design proposals, postmortems, handoffs. Glyphs should be compact and visual — one screen, dense info, no walls of text. This skill enforces that.
|
|
4
|
+
version: 4
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Glyphs
|
|
@@ -15,7 +15,7 @@ artifacts/<slug>/
|
|
|
15
15
|
└── comments.json ← free-placed pins (mutable)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Two locations
|
|
18
|
+
Two locations: `<projectRoot>/.bizar/artifacts/` (preferred) and `~/.config/opencode/artifacts/` (global).
|
|
19
19
|
|
|
20
20
|
## Frontmatter
|
|
21
21
|
|
|
@@ -28,104 +28,121 @@ kind: plan | postmortem | recap | design
|
|
|
28
28
|
---
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## The
|
|
31
|
+
## The 6 rules — read this first, every time
|
|
32
|
+
|
|
33
|
+
These are non-negotiable. The existing 357-line v3-to-v4-consolidation glyph is a cautionary tale — don't write that one.
|
|
34
|
+
|
|
35
|
+
### Rule 1 — One screen
|
|
36
|
+
|
|
37
|
+
A glyph MUST fit on one screen of the dashboard (≈ 1000px tall at desktop width). Long glyphs are skimmed and abandoned. If you can't fit the work in 5-10 blocks, you're covering too much. Break the work into multiple glyphs, one per phase.
|
|
38
|
+
|
|
39
|
+
### Rule 2 — One idea per block
|
|
40
|
+
|
|
41
|
+
If a block covers two ideas, split it. Each block is a single visual unit. The dashboard groups blocks into sections by id prefix — use that.
|
|
42
|
+
|
|
43
|
+
### Rule 3 — RichText is glue, not body
|
|
44
|
+
|
|
45
|
+
RichText is for 1-3 sentence transitions between visual blocks. NEVER use RichText for the actual content. If you find yourself writing 4+ sentences in a RichText, switch to:
|
|
46
|
+
|
|
47
|
+
- **A table** if you're listing things with attributes
|
|
48
|
+
- **A decision** if you're explaining why one option won
|
|
49
|
+
- **A file tree** if you're listing changes
|
|
50
|
+
- **A stat** if you're highlighting a number
|
|
51
|
+
- **A workflow** if you're describing a sequence
|
|
52
|
+
|
|
53
|
+
### Rule 4 — Lead with visuals
|
|
54
|
+
|
|
55
|
+
The first block after the title/headline should be a visual: a Stat, a Callout, or a Table. Never open with a long RichText. The user decides in 5 seconds whether to keep scrolling based on the visual.
|
|
56
|
+
|
|
57
|
+
### Rule 5 — One headline callout
|
|
58
|
+
|
|
59
|
+
Use `<Callout tone="success|danger">` once, near the top, as the TL;DR. Everything else supports it.
|
|
60
|
+
|
|
61
|
+
### Rule 6 — Truncate hints, not bodies
|
|
62
|
+
|
|
63
|
+
Stat `hint` props should be 1 short clause. Long hints mean the value needs to be a different block.
|
|
64
|
+
|
|
65
|
+
## The block vocabulary — minimal reference
|
|
32
66
|
|
|
33
67
|
| Block | What it shows | Required `data` |
|
|
34
68
|
| --- | --- | --- |
|
|
35
|
-
| `<RichText>` |
|
|
36
|
-
| `<Callout tone
|
|
37
|
-
| `<Stat label value trend hint />` | Big number
|
|
38
|
-
| `<Checklist items
|
|
69
|
+
| `<RichText>` | 1-3 sentences of glue | none |
|
|
70
|
+
| `<Callout tone>` | Boxed TL;DR | none (tone defaults to info) |
|
|
71
|
+
| `<Stat label value trend hint />` | Big number + label | `label`, `value` |
|
|
72
|
+
| `<Checklist items />` | Checkbox list | `items: [{id,label,checked}]` |
|
|
39
73
|
| `<Table columns rows />` | Tabular data | `columns: []`, `rows: [[]]` |
|
|
40
|
-
| `<CodeTabs tabs />` | Tabbed code
|
|
74
|
+
| `<CodeTabs tabs />` | Tabbed code | `tabs: [{id,label,language,code}]` |
|
|
41
75
|
| `<Decision title question options />` | Multi-option choice | `options: [{id,label,detail,recommended?}]` |
|
|
42
76
|
| `<OpenQuestions questions />` | Unanswered questions | `questions: [{id,label,kind,options?}]` |
|
|
43
|
-
| `<FileTree title entries />` | File-by-file
|
|
44
|
-
| `<Diff before after filename language mode />` | Before/after
|
|
77
|
+
| `<FileTree title entries />` | File-by-file changes | `entries: [{path,change,note?}]` |
|
|
78
|
+
| `<Diff before after filename language mode />` | Before/after | `before`, `after` |
|
|
45
79
|
| `<Workflow steps connections />` | Node graph | `steps: [{id,label,type}]` |
|
|
46
80
|
| `<Mockup title x y w h html />` | Inline UI mockup | `html` |
|
|
47
81
|
| `<Diagram title dataHtml dataCss />` | Inline SVG | `dataHtml` |
|
|
48
82
|
|
|
49
|
-
Every block needs a unique `id`.
|
|
83
|
+
Every block needs a unique `id`. RichText + Callout use open/close tags (markdown body); everything else is self-closing.
|
|
50
84
|
|
|
51
85
|
## Skeleton
|
|
52
86
|
|
|
53
87
|
```mdx
|
|
54
88
|
<RichText id="overview">
|
|
55
|
-
## What this
|
|
89
|
+
## What this is
|
|
56
90
|
|
|
57
|
-
|
|
91
|
+
One sentence the reader can't miss.
|
|
58
92
|
</RichText>
|
|
59
93
|
|
|
60
|
-
<Stat id="headline" label="X" value="7" trend="flat" />
|
|
94
|
+
<Stat id="headline" label="X shipped" value="7" trend="flat" hint="across v3.22 → v4.4.7" />
|
|
61
95
|
|
|
62
|
-
<Callout id="
|
|
63
|
-
One
|
|
96
|
+
<Callout id="tldr" tone="success">
|
|
97
|
+
One-sentence TL;DR.
|
|
64
98
|
</Callout>
|
|
65
99
|
|
|
100
|
+
<Table
|
|
101
|
+
id="releases"
|
|
102
|
+
columns={["v", "what", "fix"]}
|
|
103
|
+
rows={[
|
|
104
|
+
["3.22", "unified", "—"],
|
|
105
|
+
["4.4.1", "bg fix", "process exit crash"],
|
|
106
|
+
]}
|
|
107
|
+
/>
|
|
108
|
+
|
|
66
109
|
<FileTree
|
|
67
110
|
id="files"
|
|
68
111
|
title="Files changed"
|
|
69
112
|
entries={[
|
|
70
|
-
{ path: "
|
|
71
|
-
{ path: "src/bar.ts", change: "added" },
|
|
72
|
-
]}
|
|
73
|
-
/>
|
|
74
|
-
|
|
75
|
-
<Workflow
|
|
76
|
-
id="flow"
|
|
77
|
-
steps={[
|
|
78
|
-
{ id: "s1", label: "Step 1", type: "task" },
|
|
79
|
-
{ id: "s2", label: "Step 2", type: "task" },
|
|
80
|
-
]}
|
|
81
|
-
connections={[
|
|
82
|
-
{ from: "s1", to: "s2" },
|
|
113
|
+
{ path: "cli/provision.mjs", change: "added", note: "unified provisioner" },
|
|
83
114
|
]}
|
|
84
115
|
/>
|
|
85
116
|
```
|
|
86
117
|
|
|
87
118
|
## The 5 things that break a glyph
|
|
88
119
|
|
|
89
|
-
|
|
120
|
+
These shipped as bugs. Read before writing.
|
|
90
121
|
|
|
91
122
|
### 1. Backticks inside attribute strings
|
|
92
123
|
|
|
93
|
-
The parser scans for `` ` ``
|
|
124
|
+
The parser scans for `` ` `` and breaks. Use straight quotes or rephrase:
|
|
94
125
|
|
|
95
126
|
```
|
|
96
127
|
WRONG: Run `bizar install` to bootstrap.
|
|
97
128
|
RIGHT: Run 'bizar install' to bootstrap.
|
|
98
|
-
RIGHT: Run the installer to bootstrap.
|
|
99
129
|
```
|
|
100
130
|
|
|
101
|
-
### 2.
|
|
131
|
+
### 2. Braces inside string values
|
|
102
132
|
|
|
103
|
-
`{`
|
|
133
|
+
The brace counter doesn't know about strings. `{` / `}` in notes still bump depth. Keep note strings brace-free.
|
|
104
134
|
|
|
105
|
-
|
|
135
|
+
### 3. `FileTree` `change` must be exact
|
|
106
136
|
|
|
107
|
-
|
|
137
|
+
`added | modified | removed | renamed`. Empty string or `null` crashes on `t.bg`. Default to `modified` with a `note` when unclear.
|
|
108
138
|
|
|
109
|
-
|
|
139
|
+
### 4. Duplicate block ids
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
WRONG: change: null, change: "", change: "Modified"
|
|
113
|
-
RIGHT: change: "modified"
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
When in doubt, use `modified` with a `note` that explains why.
|
|
117
|
-
|
|
118
|
-
### 4. Two blocks with the same id
|
|
141
|
+
Comment pins and error reporting key off ids. Two blocks with the same id collide.
|
|
119
142
|
|
|
120
|
-
|
|
143
|
+
### 5. `## Heading <768px`
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
WRONG: ## Mobile <768px ← <7 looks like a malformed tag
|
|
126
|
-
RIGHT: ## Mobile (under 768px)
|
|
127
|
-
RIGHT: ## Mobile: under 768px
|
|
128
|
-
```
|
|
145
|
+
The parser sees `<7` as a malformed JSX tag. Use parentheses or colons.
|
|
129
146
|
|
|
130
147
|
## Validate before shipping
|
|
131
148
|
|
|
@@ -134,14 +151,13 @@ node -e "import('./bizar-dash/src/server/glyphs/mdx-compiler.mjs').then(m => { \
|
|
|
134
151
|
const fs = require('node:fs'); \
|
|
135
152
|
const out = m.compileGlyphMdxSync(fs.readFileSync('artifacts/<slug>/artifact.mdx','utf8')); \
|
|
136
153
|
console.log('blocks:', out.blocks.length, 'errors:', out.errors.length); \
|
|
137
|
-
out.errors.forEach(e => console.error(' line', e.line, ':', e.message)); \
|
|
138
154
|
})"
|
|
139
155
|
```
|
|
140
156
|
|
|
141
|
-
|
|
157
|
+
Also check yourself: does it fit on one screen? Are there any 4-sentence RichText blocks? Are the stats/tables/file trees doing the heavy lifting? If not, rewrite.
|
|
142
158
|
|
|
143
159
|
## See also
|
|
144
160
|
|
|
145
161
|
- `bizar-dash/src/server/glyphs/mdx-compiler.mjs` — parser
|
|
146
|
-
- `bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx` — React renderer (has
|
|
147
|
-
- `bizar-dash/src/web/views/glyphs/components.tsx` — block
|
|
162
|
+
- `bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx` — React renderer (has error banner)
|
|
163
|
+
- `bizar-dash/src/web/views/glyphs/components.tsx` — block implementations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.11",
|
|
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. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|