@polderlabs/bizar 4.4.10 → 4.4.12

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/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. Doctor health check ────────────────────────────────────────
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "4.4.10",
3
+ "version": "4.4.12",
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": {