@polderlabs/bizar 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/cli/audit.mjs +144 -0
  4. package/cli/banner.mjs +41 -0
  5. package/cli/bin.mjs +186 -0
  6. package/cli/copy.mjs +508 -0
  7. package/cli/export.mjs +87 -0
  8. package/cli/init.mjs +147 -0
  9. package/cli/install.mjs +390 -0
  10. package/cli/plan-templates.mjs +523 -0
  11. package/cli/plan.mjs +2087 -0
  12. package/cli/prompts.mjs +163 -0
  13. package/cli/update.mjs +273 -0
  14. package/cli/utils.mjs +153 -0
  15. package/config/AGENTS.md +282 -0
  16. package/config/agents/baldr.md +148 -0
  17. package/config/agents/forseti.md +112 -0
  18. package/config/agents/frigg.md +101 -0
  19. package/config/agents/heimdall.md +157 -0
  20. package/config/agents/hermod.md +144 -0
  21. package/config/agents/mimir.md +115 -0
  22. package/config/agents/odin.md +309 -0
  23. package/config/agents/quick.md +78 -0
  24. package/config/agents/semble-search.md +44 -0
  25. package/config/agents/thor.md +97 -0
  26. package/config/agents/tyr.md +96 -0
  27. package/config/agents/vidarr.md +100 -0
  28. package/config/agents/vor.md +140 -0
  29. package/config/commands/audit.md +1 -0
  30. package/config/commands/explain.md +1 -0
  31. package/config/commands/init.md +1 -0
  32. package/config/commands/learn.md +1 -0
  33. package/config/commands/pr-review.md +1 -0
  34. package/config/commands/tailscale-serve.md +96 -0
  35. package/config/hooks/README.md +29 -0
  36. package/config/hooks/post-tool-use.md +16 -0
  37. package/config/hooks/pre-tool-use.md +16 -0
  38. package/config/opencode.json +52 -0
  39. package/config/opencode.json.template +52 -0
  40. package/config/rules/general.md +8 -0
  41. package/config/rules/git.md +11 -0
  42. package/config/rules/javascript.md +10 -0
  43. package/config/rules/python.md +10 -0
  44. package/config/rules/testing.md +10 -0
  45. package/config/skills/bizar/README.md +9 -0
  46. package/config/skills/bizar/SKILL.md +187 -0
  47. package/config/skills/cpp-coding-standards/README.md +28 -0
  48. package/config/skills/cpp-coding-standards/SKILL.md +634 -0
  49. package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
  50. package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
  51. package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
  52. package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
  53. package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
  54. package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
  55. package/config/skills/cpp-testing/README.md +28 -0
  56. package/config/skills/cpp-testing/SKILL.md +304 -0
  57. package/config/skills/cpp-testing/agents/openai.yaml +4 -0
  58. package/config/skills/cpp-testing/references/coverage.md +370 -0
  59. package/config/skills/cpp-testing/references/framework-compare.md +175 -0
  60. package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
  61. package/config/skills/cpp-testing/references/mocking.md +364 -0
  62. package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
  63. package/config/skills/embedded-esp-idf/README.md +41 -0
  64. package/config/skills/embedded-esp-idf/SKILL.md +439 -0
  65. package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
  66. package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
  67. package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
  68. package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
  69. package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
  70. package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
  71. package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
  72. package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
  73. package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
  74. package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
  75. package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
  76. package/config/skills/self-improvement/SKILL.md +64 -0
  77. package/package.json +47 -0
  78. package/templates/plan/htmx.min.js +1 -0
  79. package/templates/plan/library/bug-investigation.mdx +79 -0
  80. package/templates/plan/library/decision-record.mdx +71 -0
  81. package/templates/plan/library/feature-design.mdx +92 -0
  82. package/templates/plan/meta.json.template +8 -0
  83. package/templates/plan/plan.canvas.template +1711 -0
  84. package/templates/plan/plan.html.template +937 -0
  85. package/templates/plan/plan.mdx.template +46 -0
@@ -0,0 +1,163 @@
1
+ import inquirer from 'inquirer';
2
+
3
+ const AGENTS_LIST = [
4
+ { name: 'Odin ᛟ — MiniMax-M3 (Router)', value: 'odin.md', checked: true },
5
+ { name: 'Frigg ᚠ — DeepSeek (Codebase Q&A, read-only)', value: 'frigg.md', checked: true },
6
+ { name: 'Vör ᚡ — DeepSeek (Clarification)', value: 'vor.md', checked: true },
7
+ { name: 'Mimir ᛗ — DeepSeek (Research)', value: 'mimir.md', checked: true },
8
+ { name: 'Heimdall ᚹ — DeepSeek (Simple tasks)', value: 'heimdall.md', checked: true },
9
+ { name: 'Hermod ᚱ — MiniMax-M2.7 (Git ops)', value: 'hermod.md', checked: true },
10
+ { name: 'Thor ᚦ — MiniMax-M2.7 (Mid impl.)', value: 'thor.md', checked: true },
11
+ { name: 'Baldr ᛒ — MiniMax-M2.7 (Design)', value: 'baldr.md', checked: true },
12
+ { name: 'Tyr ᛏ — MiniMax-M3 (Complex impl.)', value: 'tyr.md', checked: true },
13
+ { name: 'Vidarr ᛉ — GPT-5.5 (Last resort)', value: 'vidarr.md', checked: false },
14
+ { name: 'Forseti ᚨ — MiniMax-M3 (Plan audit)', value: 'forseti.md', checked: true },
15
+ ];
16
+
17
+ export async function promptComponents() {
18
+ const { components } = await inquirer.prompt([{
19
+ type: 'checkbox',
20
+ name: 'components',
21
+ message: 'What would you like to install?',
22
+ choices: [
23
+ { name: 'Agent definitions (all 11 agents)', value: 'agents', checked: true },
24
+ { name: 'AGENTS.md routing table', value: 'agents-md', checked: true },
25
+ new inquirer.Separator(),
26
+ { name: 'BizarHarness skill', value: 'skill-bizar', checked: true },
27
+ { name: 'Self-improvement skill', value: 'skill-improve', checked: true },
28
+ { name: 'C++ coding standards skill', value: 'skill-cpp-std', checked: false },
29
+ { name: 'C++ testing skill', value: 'skill-cpp-test', checked: false },
30
+ { name: 'Embedded ESP-IDF skill', value: 'skill-esp-idf', checked: true },
31
+ new inquirer.Separator(),
32
+ { name: 'Always-on rules (5 rule files)', value: 'rules', checked: true },
33
+ { name: 'Hook system (behavioral hooks)', value: 'hooks', checked: true },
34
+ { name: 'Slash commands (/explain, /audit, /learn, /pr-review, /init)', value: 'commands', checked: true },
35
+ new inquirer.Separator(),
36
+ { name: 'opencode.json (provider + MCP config)', value: 'opencode-json', checked: true },
37
+ { name: '.bizar/ folder (self-improvement log)', value: 'bizar', checked: true },
38
+ new inquirer.Separator(),
39
+ { name: 'Bizar plugin (loop guard, status, handoff signal)', value: 'plugin-bizar', checked: true },
40
+ ],
41
+ pageSize: 18,
42
+ validate(answer) {
43
+ if (answer.length === 0) return 'Select at least one component.';
44
+ return true;
45
+ },
46
+ }]);
47
+ return components;
48
+ }
49
+
50
+ export async function promptInstallMode() {
51
+ const { mode } = await inquirer.prompt([{
52
+ type: 'list',
53
+ name: 'mode',
54
+ message: 'Installation mode:',
55
+ choices: [
56
+ { name: 'Fresh install — overwrite existing config files', value: 'fresh' },
57
+ { name: 'Merge — keep your existing config, add missing files only', value: 'merge' },
58
+ ],
59
+ }]);
60
+ return mode;
61
+ }
62
+
63
+ export async function promptAgents() {
64
+ const { agents } = await inquirer.prompt([{
65
+ type: 'checkbox',
66
+ name: 'agents',
67
+ message: 'Select agents to install:',
68
+ choices: AGENTS_LIST,
69
+ pageSize: 12,
70
+ validate(answer) {
71
+ if (answer.length === 0) return 'Select at least one agent.';
72
+ return true;
73
+ },
74
+ }]);
75
+ return agents;
76
+ }
77
+
78
+ export async function promptApiKeys() {
79
+ const { setup } = await inquirer.prompt([{
80
+ type: 'confirm',
81
+ name: 'setup',
82
+ message: 'Configure API keys now?',
83
+ default: false,
84
+ }]);
85
+ if (!setup) return {};
86
+
87
+ const keys = await inquirer.prompt([
88
+ {
89
+ type: 'input',
90
+ name: 'opencodeZen',
91
+ message: 'OpenCode Zen API key (DeepSeek V4 Flash Free):',
92
+ validate: v => v.length > 0 || 'Required for free-tier agents (Mimir, Heimdall, Vör)',
93
+ },
94
+ {
95
+ type: 'input',
96
+ name: 'minimax',
97
+ message: 'MiniMax API key (M2.7 + M3):',
98
+ validate: v => v.length > 0 || 'Required for Odin, Thor, Hermod, Baldr, Tyr, Forseti',
99
+ },
100
+ {
101
+ type: 'input',
102
+ name: 'openai',
103
+ message: 'OpenAI API key (GPT-5.5 — optional for Vidarr):',
104
+ },
105
+ {
106
+ type: 'input',
107
+ name: 'hindsight',
108
+ message: 'Hindsight API key (memory persistence):',
109
+ validate: v => v.length > 0 || 'Required for cross-session memory',
110
+ },
111
+ {
112
+ type: 'input',
113
+ name: 'semble',
114
+ message: 'Semble API key (code search — or leave blank for local only):',
115
+ },
116
+ ]);
117
+ return keys;
118
+ }
119
+
120
+ export async function promptConfirmInstall(summary) {
121
+ const { ok } = await inquirer.prompt([{
122
+ type: 'confirm',
123
+ name: 'ok',
124
+ message: `Install ${summary.components} ${summary.agents} into ${summary.target}?`,
125
+ default: true,
126
+ }]);
127
+ return ok;
128
+ }
129
+
130
+ export async function promptRestartOpenCode() {
131
+ const { restart } = await inquirer.prompt([{
132
+ type: 'confirm',
133
+ name: 'restart',
134
+ message: 'Restart opencode now to pick up changes?',
135
+ default: true,
136
+ }]);
137
+ return restart;
138
+ }
139
+
140
+ export async function promptSkillPacks() {
141
+ const { usePacks } = await inquirer.prompt([{
142
+ type: 'confirm',
143
+ name: 'usePacks',
144
+ message: 'Install curated skills from skills.sh ecosystem? (find-skills, React, Supabase, TDD, design, etc.)',
145
+ default: true,
146
+ }]);
147
+ if (!usePacks) return [];
148
+
149
+ const { packs } = await inquirer.prompt([{
150
+ type: 'checkbox',
151
+ name: 'packs',
152
+ message: 'Select skill packs to install:',
153
+ choices: [
154
+ { name: 'Core — find-skills, skill-creator, write-a-skill', value: 'core', checked: true },
155
+ { name: 'Frontend — React, web-design, composition, a11y, shadcn/ui', value: 'frontend', checked: false },
156
+ { name: 'Backend — Supabase, Postgres, API patterns, auth', value: 'backend', checked: false },
157
+ { name: 'Testing — TDD, E2E, Playwright, test patterns', value: 'testing', checked: false },
158
+ { name: 'Design — frontend-design, UI/UX, taste skills', value: 'design', checked: false },
159
+ ],
160
+ pageSize: 8,
161
+ }]);
162
+ return packs;
163
+ }
package/cli/update.mjs ADDED
@@ -0,0 +1,273 @@
1
+ /**
2
+ * update.mjs — `bizar update` subcommand.
3
+ *
4
+ * Updates the opencode CLI, the @polderlabs/bizar package, and/or the
5
+ * @polderlabs/bizar-plugin package. By default, prompts for each
6
+ * component individually. With `--all`, updates everything without
7
+ * prompting. With explicit subcommands (`opencode`, `bizar`, `plugin`),
8
+ * runs only the named update.
9
+ *
10
+ * Each update is a thin wrapper around the underlying installer:
11
+ * - opencode: `opencode upgrade` (or `npm install -g opencode-ai@latest` as a fallback)
12
+ * - bizar: `npm install -g @polderlabs/bizar@latest`
13
+ * - plugin: `npm install -g @polderlabs/bizar-plugin@latest`
14
+ *
15
+ * After the package updates, re-runs the install script so the locally
16
+ * deployed plugin source matches the just-upgraded npm version. Without
17
+ * this, the plugin in `~/.config/opencode/plugins/bizar/` would be one
18
+ * version behind the npm registry, and the BUGS.md "version skew"
19
+ * trap would bite.
20
+ *
21
+ * Exit codes:
22
+ * 0 — all requested updates succeeded (or none were requested)
23
+ * 1 — at least one update failed
24
+ */
25
+
26
+ import chalk from 'chalk';
27
+ import { execSync, spawnSync } from 'node:child_process';
28
+
29
+ const PKG_MAIN = '@polderlabs/bizar';
30
+ const PKG_PLUGIN = '@polderlabs/bizar-plugin';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Version helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Read the currently installed version of an npm package.
38
+ * Returns `null` if not installed globally.
39
+ */
40
+ function currentVersion(pkg) {
41
+ try {
42
+ const out = execSync(`npm ls -g ${pkg} --depth=0 --json`, { stdio: ['ignore', 'pipe', 'ignore'] })
43
+ .toString();
44
+ const parsed = JSON.parse(out);
45
+ const deps = parsed.dependencies ?? {};
46
+ return deps[pkg]?.version ?? null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Read the latest version of an npm package from the registry.
54
+ * Returns `null` if the registry is unreachable or the package is not published.
55
+ */
56
+ function latestVersion(pkg) {
57
+ try {
58
+ const out = execSync(`npm view ${pkg} version`, { stdio: ['ignore', 'pipe', 'ignore'] })
59
+ .toString()
60
+ .trim();
61
+ return out || null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Update actions
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Update opencode. Tries `opencode upgrade` first (the upstream installer's
73
+ * own command); falls back to `npm install -g opencode-ai@latest`.
74
+ * Returns `{ ok: boolean, message: string }`.
75
+ */
76
+ function updateOpencode() {
77
+ // Try `opencode upgrade` first — that's the canonical updater.
78
+ const r1 = spawnSync('opencode', ['upgrade'], { stdio: 'inherit' });
79
+ if (r1.status === 0) {
80
+ return { ok: true, message: 'opencode updated via `opencode upgrade`' };
81
+ }
82
+ // Fall back to npm. `opencode-ai` is the npm package name.
83
+ console.log(chalk.dim(' opencode upgrade not available; falling back to npm'));
84
+ const r2 = spawnSync('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit' });
85
+ if (r2.status === 0) {
86
+ return { ok: true, message: 'opencode updated via npm' };
87
+ }
88
+ return { ok: false, message: 'opencode update failed — try `opencode upgrade` manually' };
89
+ }
90
+
91
+ /**
92
+ * Update @polderlabs/bizar globally. Returns `{ ok, message }`.
93
+ */
94
+ function updateBizar() {
95
+ const r = spawnSync('npm', ['install', '-g', `${PKG_MAIN}@latest`], { stdio: 'inherit' });
96
+ if (r.status !== 0) {
97
+ return { ok: false, message: `${PKG_MAIN} update failed` };
98
+ }
99
+ return { ok: true, message: `${PKG_MAIN} updated` };
100
+ }
101
+
102
+ /**
103
+ * Update @polderlabs/bizar-plugin globally. Returns `{ ok, message }`.
104
+ */
105
+ function updateBizarPlugin() {
106
+ const r = spawnSync('npm', ['install', '-g', `${PKG_PLUGIN}@latest`], { stdio: 'inherit' });
107
+ if (r.status !== 0) {
108
+ return { ok: false, message: `${PKG_PLUGIN} update failed` };
109
+ }
110
+ return { ok: true, message: `${PKG_PLUGIN} updated` };
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Re-run the install script so the locally deployed plugin matches npm
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * After updating the npm packages, re-run the install script so the
119
+ * plugin source on disk matches the just-installed version. This is the
120
+ * fix for the BUGS.md "version skew" trap.
121
+ */
122
+ function rerunInstallScript() {
123
+ // Find the package root via `npm root -g` + package name. This works
124
+ // whether the user installed via `npm install -g` or `npx`.
125
+ let globalRoot;
126
+ try {
127
+ globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
128
+ .toString()
129
+ .trim();
130
+ } catch {
131
+ return { ok: false, message: 'could not locate npm global root; skipping install-script rerun' };
132
+ }
133
+ const pkgRoot = `${globalRoot}/${PKG_MAIN}`;
134
+ const installSh = `${pkgRoot}/install.sh`;
135
+ console.log(chalk.dim(`\n Re-running install script at ${installSh}...`));
136
+ const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
137
+ if (r.status !== 0) {
138
+ return { ok: false, message: 'install script rerun failed' };
139
+ }
140
+ return { ok: true, message: 'install script re-run' };
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Prompt helpers
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * Interactive prompt for which components to update. Uses inquirer.
149
+ * Returns a Set of `opencode`, `bizar`, `plugin` strings.
150
+ */
151
+ async function promptForUpdates(forceAll) {
152
+ if (forceAll) {
153
+ return new Set(['opencode', 'bizar', 'plugin']);
154
+ }
155
+ // Dynamic import so the rest of the file can be loaded even if
156
+ // inquirer is unavailable for some reason.
157
+ const inquirer = await import('inquirer');
158
+ const { selections } = await inquirer.default.prompt([
159
+ {
160
+ type: 'checkbox',
161
+ name: 'selections',
162
+ message: 'Which components do you want to update?',
163
+ choices: [
164
+ { name: 'opencode (the opencode CLI itself)', value: 'opencode', checked: true },
165
+ { name: `bizar (${PKG_MAIN})`, value: 'bizar', checked: true },
166
+ { name: `plugin (${PKG_PLUGIN})`, value: 'plugin', checked: true },
167
+ ],
168
+ },
169
+ ]);
170
+ return new Set(selections);
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Main entry point
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /**
178
+ * Run the update subcommand.
179
+ *
180
+ * @param {string[]} subargs - arguments after `bizar update`
181
+ */
182
+ export async function runUpdate(subargs = []) {
183
+ console.log(chalk.bold.hex('#a855f7')('\n ᚦ BIZAR UPDATE ᚦ\n'));
184
+
185
+ // Show what we know before asking.
186
+ const cur = {
187
+ opencode: currentVersion('opencode-ai'),
188
+ bizar: currentVersion(PKG_MAIN),
189
+ plugin: currentVersion(PKG_PLUGIN),
190
+ };
191
+ const latest = {
192
+ opencode: latestVersion('opencode-ai'),
193
+ bizar: latestVersion(PKG_MAIN),
194
+ plugin: latestVersion(PKG_PLUGIN),
195
+ };
196
+
197
+ console.log(' Installed vs. latest:');
198
+ for (const k of Object.keys(cur)) {
199
+ const label = k === 'plugin' ? PKG_PLUGIN : k === 'bizar' ? PKG_MAIN : 'opencode-ai';
200
+ const c = cur[k] ?? '(not installed)';
201
+ const l = latest[k] ?? '(unknown)';
202
+ const same = c === l;
203
+ console.log(` ${label.padEnd(28)} ${c.padEnd(15)} → ${l}${same ? ' ✓ up to date' : ' ⤵ update available'}`);
204
+ }
205
+ console.log('');
206
+
207
+ // Decide what to update.
208
+ let selected;
209
+ if (subargs.includes('--all')) {
210
+ selected = new Set(['opencode', 'bizar', 'plugin']);
211
+ } else if (subargs.length === 0) {
212
+ selected = await promptForUpdates(false);
213
+ } else {
214
+ // Explicit subcommands.
215
+ const valid = new Set(['opencode', 'bizar', 'plugin']);
216
+ selected = new Set(subargs.filter((a) => valid.has(a)));
217
+ if (selected.size === 0) {
218
+ console.log(chalk.yellow(` No valid components selected from: ${subargs.join(' ')}`));
219
+ console.log(chalk.dim(' Valid components: opencode, bizar, plugin'));
220
+ console.log(chalk.dim(' Run `bizar update --help` for usage.'));
221
+ process.exit(1);
222
+ }
223
+ }
224
+
225
+ if (selected.size === 0) {
226
+ console.log(chalk.dim(' Nothing to update.'));
227
+ return;
228
+ }
229
+
230
+ console.log(chalk.cyan(` Updating: ${[...selected].join(', ')}\n`));
231
+
232
+ const results = [];
233
+ if (selected.has('opencode')) {
234
+ console.log(chalk.bold(' → opencode'));
235
+ results.push(['opencode', updateOpencode()]);
236
+ }
237
+ if (selected.has('bizar')) {
238
+ console.log(chalk.bold(` → ${PKG_MAIN}`));
239
+ results.push(['bizar', updateBizar()]);
240
+ }
241
+ if (selected.has('plugin')) {
242
+ console.log(chalk.bold(` → ${PKG_PLUGIN}`));
243
+ results.push(['plugin', updateBizarPlugin()]);
244
+ }
245
+
246
+ // Re-run the install script if anything was updated, so the deployed
247
+ // plugin source matches the registry.
248
+ const anySuccess = results.some(([, r]) => r.ok);
249
+ if (anySuccess && (selected.has('bizar') || selected.has('plugin'))) {
250
+ console.log('');
251
+ const rerun = rerunInstallScript();
252
+ if (rerun.ok) {
253
+ console.log(chalk.green(`\n ✓ ${rerun.message}`));
254
+ } else {
255
+ console.log(chalk.yellow(`\n ⚠ ${rerun.message}`));
256
+ console.log(chalk.dim(' Run `bash install.sh` from the Bizar repo manually.'));
257
+ }
258
+ }
259
+
260
+ console.log('');
261
+ console.log(' Summary:');
262
+ for (const [name, r] of results) {
263
+ const marker = r.ok ? chalk.green('✓') : chalk.red('✗');
264
+ console.log(` ${marker} ${name.padEnd(10)} ${r.message}`);
265
+ }
266
+
267
+ const anyFail = results.some(([, r]) => !r.ok);
268
+ if (anyFail) {
269
+ console.log(chalk.yellow('\n Some updates failed. See messages above.'));
270
+ process.exit(1);
271
+ }
272
+ console.log(chalk.green('\n ✓ Update complete\n'));
273
+ }
package/cli/utils.mjs ADDED
@@ -0,0 +1,153 @@
1
+ import { access, constants } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
7
+ const REPO_ROOT = join(__dirname, '..');
8
+
9
+ const isWin = process.platform === 'win32';
10
+
11
+ export function repoPath(...parts) {
12
+ return join(REPO_ROOT, ...parts);
13
+ }
14
+
15
+ export function opencodeConfigDir() {
16
+ if (isWin) {
17
+ return process.env.APPDATA
18
+ ? join(process.env.APPDATA, 'opencode')
19
+ : join(homedir(), '.config', 'opencode');
20
+ }
21
+ return process.env.XDG_CONFIG_HOME
22
+ ? join(process.env.XDG_CONFIG_HOME, 'opencode')
23
+ : join(homedir(), '.config', 'opencode');
24
+ }
25
+
26
+ export function opencodeAgentsDir() {
27
+ return join(opencodeConfigDir(), 'agents');
28
+ }
29
+
30
+ async function tryReadVersion(filePath) {
31
+ try {
32
+ const pkg = await import(filePath);
33
+ return pkg.version || '';
34
+ } catch {
35
+ return '';
36
+ }
37
+ }
38
+
39
+ export async function detectOpenCode() {
40
+ const configDir = opencodeConfigDir();
41
+ const agentsDir = opencodeAgentsDir();
42
+
43
+ let exists = false;
44
+ let version = '';
45
+
46
+ try {
47
+ await access(configDir, constants.F_OK);
48
+ exists = true;
49
+
50
+ if (isWin) {
51
+ const winPaths = [
52
+ join(process.env.APPDATA || homedir(), 'npm', 'node_modules', 'opencode', 'package.json'),
53
+ join(process.env.APPDATA || homedir(), 'npm-global', 'node_modules', 'opencode', 'package.json'),
54
+ join(homedir(), 'node_modules', 'opencode', 'package.json'),
55
+ ];
56
+ for (const p of winPaths) {
57
+ version = await tryReadVersion(p);
58
+ if (version) break;
59
+ }
60
+ } else {
61
+ const posixPaths = [
62
+ join(homedir(), '.local', 'share', 'opencode', 'package.json'),
63
+ '/usr/local/lib/node_modules/opencode/package.json',
64
+ '/usr/lib/node_modules/opencode/package.json',
65
+ join(homedir(), '.npm-global', 'lib', 'node_modules', 'opencode', 'package.json'),
66
+ join(homedir(), 'node_modules', 'opencode', 'package.json'),
67
+ ];
68
+ for (const p of posixPaths) {
69
+ version = await tryReadVersion(p);
70
+ if (version) break;
71
+ }
72
+ }
73
+ } catch {
74
+ exists = false;
75
+ }
76
+
77
+ return { exists, version, configDir, agentsDir };
78
+ }
79
+
80
+ export async function detectRtk() {
81
+ const { execSync } = await import('node:child_process');
82
+ try {
83
+ execSync('rtk --version', { stdio: 'pipe' });
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ export async function detectSemble() {
91
+ const { execSync } = await import('node:child_process');
92
+ try {
93
+ execSync('uvx --from "semble[mcp]" semble --help', { stdio: 'pipe', timeout: 15000 });
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ export async function detectUv() {
101
+ const { execSync } = await import('node:child_process');
102
+ try {
103
+ execSync('uv --version', { stdio: 'pipe' });
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ export async function detectSkillsCli() {
111
+ const { execSync } = await import('node:child_process');
112
+ try {
113
+ execSync('npx --yes skills --help', { stdio: 'pipe', timeout: 30000 });
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ export async function detectInstalledAgents() {
121
+ const agentsDir = opencodeAgentsDir();
122
+ try {
123
+ await access(agentsDir, constants.F_OK);
124
+ return agentsDir;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ export function buildSummary(components, agents, target, skillPacks = []) {
131
+ const parts = [];
132
+ if (components.includes('agents')) parts.push(`${agents.length} agents`);
133
+ if (components.includes('agents-md')) parts.push('AGENTS.md');
134
+ if (components.includes('skill-bizar')) parts.push('bizar skill');
135
+ if (components.includes('skill-improve')) parts.push('self-improvement skill');
136
+ if (components.includes('opencode-json')) parts.push('opencode.json');
137
+ if (components.includes('bizar')) parts.push('.bizar/ folder');
138
+ if (components.includes('plugin-bizar')) parts.push('Bizar plugin');
139
+ if (components.includes('rules')) parts.push('rules');
140
+ if (components.includes('hooks')) parts.push('hooks');
141
+ if (components.includes('commands')) parts.push('commands');
142
+ parts.push('RTK');
143
+ parts.push('Semble');
144
+ parts.push('Skills CLI');
145
+ if (skillPacks.length > 0) parts.push(`skills: ${skillPacks.join(', ')}`);
146
+
147
+ return {
148
+ components: parts.join(', '),
149
+ agents: agents.length === 11 ? '(all 11)' : `(${agents.length} selected)`,
150
+ target,
151
+ parts,
152
+ };
153
+ }