@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
package/cli/bin.mjs ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from 'chalk';
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { runInstaller, runPostInstall } from './install.mjs';
7
+ import { runAudit } from './audit.mjs';
8
+ import { runInit } from './init.mjs';
9
+ import { runExport } from './export.mjs';
10
+ import runPlan from './plan.mjs';
11
+ import { runUpdate } from './update.mjs';
12
+
13
+ const args = process.argv.slice(2);
14
+
15
+ function showHelp() {
16
+ console.log(`
17
+ Bizar — Norse Pantheon Agent System for opencode
18
+
19
+ Usage:
20
+ bizar Run interactive installer
21
+ bizar audit Run security audit on agent configuration
22
+ bizar init Initialize .bizar/ in current project
23
+ bizar export [target] Export agents/rules to another harness (claude|cursor|opencode)
24
+ bizar plan <subcommand> Manage visual plans (new, open, list, delete, export, templates)
25
+ bizar test-gate Detect & run the project's test suite
26
+ bizar update Update opencode, bizar, and/or bizar-plugin
27
+ bizar --help Show this help
28
+
29
+ Install:
30
+ npm install -g @polderlabs/bizar Install globally, then run 'bizar'
31
+ npm install -g @polderlabs/bizar-plugin Install the Bizar opencode plugin
32
+ npx @polderlabs/bizar Run without installing
33
+ `);
34
+ }
35
+
36
+ function showAuditHelp() {
37
+ console.log(`
38
+ bizar audit — Run security audit on agent configuration
39
+
40
+ Usage:
41
+ bizar audit
42
+
43
+ Description:
44
+ Scans opencode agent definitions for security issues:
45
+ - Agents with read/edit/bash permission conflicts
46
+ - Agents lacking mode or model definitions
47
+ - Suspicious tool access patterns
48
+ `);
49
+ }
50
+
51
+ function showInitHelp() {
52
+ console.log(`
53
+ bizar init — Initialize .bizar/ in current project
54
+
55
+ Usage:
56
+ bizar init
57
+
58
+ Description:
59
+ Creates a .bizar/ directory in the current project with:
60
+ - PROJECT.md (living project description)
61
+ - AGENTS_SELF_IMPROVEMENT.md (lesson log)
62
+ `);
63
+ }
64
+
65
+ function showExportHelp() {
66
+ console.log(`
67
+ bizar export — Export agents/rules to another harness
68
+
69
+ Usage:
70
+ bizar export --target <harness>
71
+
72
+ Targets:
73
+ claude Export to Claude Code format
74
+ cursor Export to Cursor format
75
+ opencode Export to OpenCode format (default)
76
+
77
+ Description:
78
+ Converts Bizar agent definitions and rules
79
+ to the target harness's native format.
80
+ `);
81
+ }
82
+
83
+ function showTestGateHelp() {
84
+ console.log(`
85
+ bizar test-gate — Detect & run the project's test suite
86
+
87
+ Usage:
88
+ bizar test-gate
89
+
90
+ Description:
91
+ Auto-detects the project's test framework (npm test, pytest,
92
+ cargo test, go test) and runs it. Exits with non-zero on failure.
93
+ `);
94
+ }
95
+
96
+ function showUpdateHelp() {
97
+ console.log(`
98
+ bizar update — Update opencode, bizar, and/or bizar-plugin
99
+
100
+ Usage:
101
+ bizar update Interactive: ask which components to update
102
+ bizar update --all Update everything non-interactively
103
+ bizar update opencode Update only opencode
104
+ bizar update bizar Update only the bizar CLI package
105
+ bizar update plugin Update only the @polderlabs/bizar-plugin package
106
+
107
+ Description:
108
+ By default, prompts for each component (opencode, @polderlabs/bizar,
109
+ @polderlabs/bizar-plugin) before running its update. With --all, runs
110
+ every update without prompting. With explicit subcommands, runs only
111
+ the named update.
112
+
113
+ "opencode" here means the underlying opencode CLI on $PATH — the
114
+ update uses the opencode installer's own update command.
115
+ "@polderlabs/bizar" and "@polderlabs/bizar-plugin" are updated via
116
+ 'npm install -g <pkg>@latest'.
117
+ `);
118
+ }
119
+
120
+ async function runTestGate() {
121
+ console.log(chalk.bold.hex('#a855f7')('\n ᚦ TEST GATE ᚦ\n'));
122
+ const { execSync } = await import('node:child_process');
123
+
124
+ const cwd = process.cwd();
125
+ const possible = [
126
+ { cmd: 'npm test', check: 'package.json' },
127
+ { cmd: 'pytest', check: 'pyproject.toml' },
128
+ { cmd: 'cargo test', check: 'Cargo.toml' },
129
+ { cmd: 'go test ./...', check: 'go.mod' },
130
+ ];
131
+
132
+ for (const suite of possible) {
133
+ try {
134
+ if (existsSync(join(cwd, suite.check))) {
135
+ console.log(` Running: ${suite.cmd}`);
136
+ execSync(suite.cmd, { stdio: 'inherit', timeout: 120000, cwd });
137
+ console.log('\n ✓ Test gate passed\n');
138
+ return true;
139
+ }
140
+ } catch {
141
+ console.log(`\n ✗ Test gate failed: ${suite.cmd}\n`);
142
+ process.exit(1);
143
+ }
144
+ }
145
+
146
+ console.log(' No test suite detected. Install one to use the test gate.\n');
147
+ return false;
148
+ }
149
+
150
+ function parseFlag(name) {
151
+ const idx = args.indexOf(name);
152
+ if (idx === -1) return null;
153
+ return args[idx + 1] || null;
154
+ }
155
+
156
+ if (args.includes('--postinstall')) {
157
+ await runPostInstall();
158
+ } else if (args[0] === 'audit') {
159
+ if (args.includes('--help') || args.includes('-h')) showAuditHelp();
160
+ else await runAudit();
161
+ } else if (args[0] === 'init') {
162
+ if (args.includes('--help') || args.includes('-h')) showInitHelp();
163
+ else await runInit(process.cwd());
164
+ } else if (args[0] === 'export') {
165
+ if (args.includes('--help') || args.includes('-h')) showExportHelp();
166
+ else {
167
+ const target = parseFlag('--target');
168
+ await runExport(target);
169
+ }
170
+ } else if (args[0] === 'test-gate') {
171
+ if (args.includes('--help') || args.includes('-h')) showTestGateHelp();
172
+ else await runTestGate();
173
+ } else if (args[0] === 'update') {
174
+ if (args.includes('--help') || args.includes('-h')) {
175
+ showUpdateHelp();
176
+ } else {
177
+ await runUpdate(args.slice(1));
178
+ }
179
+ } else if (args[0] === 'plan') {
180
+ const planArgs = args.slice(1);
181
+ await runPlan(planArgs, {});
182
+ } else if (args.includes('--help') || args.includes('-h')) {
183
+ showHelp();
184
+ } else {
185
+ await runInstaller();
186
+ }
package/cli/copy.mjs ADDED
@@ -0,0 +1,508 @@
1
+ import { mkdir, writeFile, readFile, copyFile, access, constants } from 'node:fs/promises';
2
+ import { join, dirname, basename } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import ora from 'ora';
5
+ import chalk from 'chalk';
6
+ import { repoPath, opencodeConfigDir, opencodeAgentsDir, detectRtk, detectSemble, detectUv, detectSkillsCli } from './utils.mjs';
7
+
8
+ async function fileExists(path) {
9
+ try {
10
+ await access(path, constants.F_OK);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export async function installAgents(agentFiles, mode) {
18
+ const spinner = ora({ text: 'Installing agent definitions...', color: 'magenta' }).start();
19
+ const destDir = opencodeAgentsDir();
20
+ await mkdir(destDir, { recursive: true });
21
+
22
+ let count = 0;
23
+ for (const file of agentFiles) {
24
+ const src = repoPath('config', 'agents', file);
25
+ const dest = join(destDir, file);
26
+ if (mode === 'merge' && await fileExists(dest)) {
27
+ spinner.text = ` Skipping ${file} (already exists)`;
28
+ continue;
29
+ }
30
+ await copyFile(src, dest);
31
+ count++;
32
+ }
33
+
34
+ spinner.succeed(chalk.green(`Installed ${count} agent${count !== 1 ? 's' : ''}`));
35
+ return count;
36
+ }
37
+
38
+ export async function installAgentsMd(mode) {
39
+ const spinner = ora({ text: 'Installing AGENTS.md routing table...', color: 'magenta' }).start();
40
+ const src = repoPath('config', 'AGENTS.md');
41
+ const dest = join(opencodeConfigDir(), 'AGENTS.md');
42
+
43
+ if (mode === 'merge' && await fileExists(dest)) {
44
+ spinner.warn(chalk.yellow('AGENTS.md already exists — skipping (use fresh mode to overwrite)'));
45
+ return false;
46
+ }
47
+
48
+ await mkdir(opencodeConfigDir(), { recursive: true });
49
+ await copyFile(src, dest);
50
+ spinner.succeed(chalk.green('AGENTS.md installed'));
51
+ return true;
52
+ }
53
+
54
+ export async function installSkill(name) {
55
+ const spinner = ora({ text: `Installing ${name} skill...`, color: 'cyan' }).start();
56
+ const srcDir = repoPath('config', 'skills', name);
57
+
58
+ try {
59
+ await access(srcDir, constants.F_OK);
60
+ } catch {
61
+ spinner.warn(chalk.yellow(`Skill ${name} not found — skipping`));
62
+ return false;
63
+ }
64
+
65
+ const skillsDir = join(homedir(), '.opencode', 'skills');
66
+ const dstDir = join(skillsDir, name);
67
+ await mkdir(dstDir, { recursive: true });
68
+
69
+ const entries = await readdirRecursive(srcDir);
70
+ for (const entry of entries) {
71
+ const src = join(srcDir, entry);
72
+ const dst = join(dstDir, entry);
73
+ const dstParent = dirname(dst);
74
+ await mkdir(dstParent, { recursive: true });
75
+ await copyFile(src, dst);
76
+ }
77
+
78
+ spinner.succeed(chalk.green(`${name} skill installed`));
79
+ return true;
80
+ }
81
+
82
+ async function readdirRecursive(dir) {
83
+ const { readdir, stat } = await import('node:fs/promises');
84
+ const { join, relative } = await import('node:path');
85
+ const files = [];
86
+ async function walk(d) {
87
+ const entries = await readdir(d, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ const full = join(d, entry.name);
90
+ if (entry.isDirectory()) {
91
+ await walk(full);
92
+ } else {
93
+ files.push(relative(dir, full));
94
+ }
95
+ }
96
+ }
97
+ await walk(dir);
98
+ return files;
99
+ }
100
+
101
+ export async function installOpencodeJson(mode) {
102
+ const spinner = ora({ text: 'Configuring opencode.json...', color: 'yellow' }).start();
103
+ const template = repoPath('config', 'opencode.json');
104
+ const dest = join(opencodeConfigDir(), 'opencode.json');
105
+ await mkdir(opencodeConfigDir(), { recursive: true });
106
+
107
+ const templateRaw = await readFile(template, 'utf-8');
108
+ let templateObj;
109
+ try { templateObj = JSON.parse(templateRaw); } catch {
110
+ spinner.fail(chalk.red('Invalid opencode.json template'));
111
+ return false;
112
+ }
113
+
114
+ if (mode === 'fresh' || !(await fileExists(dest))) {
115
+ await writeFile(dest, JSON.stringify(templateObj, null, 2));
116
+ spinner.succeed(chalk.green('opencode.json configured'));
117
+ return true;
118
+ }
119
+
120
+ // Merge mode: deep merge with existing
121
+ try {
122
+ const existingRaw = await readFile(dest, 'utf-8');
123
+ const existing = JSON.parse(existingRaw);
124
+ const merged = deepMerge(existing, templateObj);
125
+ await writeFile(dest, JSON.stringify(merged, null, 2));
126
+ spinner.succeed(chalk.green('opencode.json merged (existing keys preserved)'));
127
+ return true;
128
+ } catch {
129
+ // If existing is invalid JSON, backup and overwrite
130
+ const backup = dest + '.bak';
131
+ await copyFile(dest, backup);
132
+ await writeFile(dest, JSON.stringify(templateObj, null, 2));
133
+ spinner.succeed(chalk.green('opencode.json written (backup at opencode.json.bak)'));
134
+ return true;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * The 7 BizarHarness tool keys that must be enabled in the user's opencode.json.
140
+ * These are merged idempotently — existing tool keys are never overwritten.
141
+ */
142
+ const BIZAR_TOOLS = {
143
+ bizar_plan_action: true,
144
+ bizar_get_plan_comments: true,
145
+ bizar_wait_for_feedback: true,
146
+ bizar_spawn_background: true,
147
+ bizar_status: true,
148
+ bizar_collect: true,
149
+ bizar_kill: true,
150
+ };
151
+
152
+ /**
153
+ * Idempotently merge the 7 BizarHarness tool keys into the user's
154
+ * `~/.config/opencode/opencode.json` (or the platform-equivalent config dir).
155
+ * Does NOT overwrite any other user config.
156
+ * Logs a diff of what was added.
157
+ */
158
+ export async function mergeToolsIntoUserConfig() {
159
+ const dest = join(opencodeConfigDir(), 'opencode.json');
160
+
161
+ // If user has no config yet, nothing to merge into
162
+ if (!(await fileExists(dest))) {
163
+ return { merged: false, reason: 'no-existing-config' };
164
+ }
165
+
166
+ try {
167
+ const existingRaw = await readFile(dest, 'utf-8');
168
+ const existing = JSON.parse(existingRaw);
169
+
170
+ const tools = existing.tools || {};
171
+ const added = [];
172
+ for (const [key, value] of Object.entries(BIZAR_TOOLS)) {
173
+ if (!(key in tools)) {
174
+ tools[key] = value;
175
+ added.push(key);
176
+ }
177
+ }
178
+
179
+ if (added.length === 0) {
180
+ return { merged: false, reason: 'all-keys-present' };
181
+ }
182
+
183
+ const merged = { ...existing, tools };
184
+ await writeFile(dest, JSON.stringify(merged, null, 2));
185
+
186
+ console.log(chalk.dim(` [tools] added: ${added.join(', ')}`));
187
+ return { merged: true, added };
188
+ } catch (err) {
189
+ return { merged: false, reason: 'error', error: err.message };
190
+ }
191
+ }
192
+
193
+ function deepMerge(target, source) {
194
+ const out = { ...target };
195
+ for (const [key, value] of Object.entries(source)) {
196
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
197
+ out[key] = deepMerge(out[key] || {}, value);
198
+ } else if (!(key in out)) {
199
+ out[key] = value;
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+
205
+ export async function installBizarFolder() {
206
+ const spinner = ora({ text: 'Setting up .bizar/ folder...', color: 'cyan' }).start();
207
+ const srcDir = repoPath('.bizar');
208
+ try {
209
+ await access(srcDir, constants.F_OK);
210
+ } catch {
211
+ spinner.warn(chalk.yellow('.bizar/ not found in repo — skipping'));
212
+ return false;
213
+ }
214
+
215
+ const cwd = process.cwd();
216
+ const destDir = join(cwd, '.bizar');
217
+ await mkdir(destDir, { recursive: true });
218
+
219
+ const files = await readdirRecursive(srcDir);
220
+ for (const file of files) {
221
+ const src = join(srcDir, file);
222
+ const dst = join(destDir, file);
223
+ await copyFile(src, dst);
224
+ }
225
+
226
+ spinner.succeed(chalk.green('.bizar/ folder created in current directory'));
227
+ return true;
228
+ }
229
+
230
+ /**
231
+ * Install the Bizar plugin into a project's .opencode/ directory.
232
+ *
233
+ * Per spec §9.2:
234
+ * - Copies plugins/bizar/ → <project>/.opencode/plugins/bizar/
235
+ * - Excludes: node_modules/, dist/, *.log, .DS_Store
236
+ * - Returns { copied: number, errors: string[] }
237
+ *
238
+ * INSTALL PATH CONCLUSION (step 5 of the Heimdall wiring task):
239
+ * The install target is INSIDE the project at <project>/.opencode/plugins/bizar/,
240
+ * NOT in the system config dir (~/.config/opencode/plugins/bizar/).
241
+ * Rationale:
242
+ * 1. Per-project isolation — each project pins its own plugin version
243
+ * (spec §9.1: "Per-project isolation (each project can pin its own
244
+ * plugin version)").
245
+ * 2. Follows opencode's project-local config convention — the config file
246
+ * is at <project>/.opencode/opencode.json and the plugin ref is
247
+ * `./plugins/bizar/index.ts` relative to that config dir.
248
+ * 3. Mirrors the .bizar/ folder pattern (project-local, not system-wide).
249
+ * 4. Not "polluting" — the .opencode/ directory is the standard location
250
+ * for project-local opencode config (analogous to .vscode/).
251
+ * 5. In the Docker sandbox (BizarHarness-dev), the host project is mounted
252
+ * at /project, so /project/.opencode/plugins/bizar/ resolves correctly
253
+ * relative to /project/.opencode/opencode.json.
254
+ *
255
+ * The source (<repo>/plugins/bizar/) is copied FROM the harness repo INTO
256
+ * the project's .opencode/ directory. After install, the harness repo is no
257
+ * longer the authoritative source — the project has its own copy.
258
+ *
259
+ * See spec §9.1 for the canonical layout diagram.
260
+ *
261
+ * @param {string} [projectRoot] - Project root directory (defaults to cwd)
262
+ * @returns {Promise<{copied: number, errors: string[]}>}
263
+ */
264
+ export async function installPluginBizar(projectRoot) {
265
+ if (!projectRoot) projectRoot = process.cwd();
266
+ const spinner = ora({ text: 'Installing Bizar plugin...', color: 'cyan' }).start();
267
+ const srcDir = repoPath('plugins', 'bizar');
268
+
269
+ try {
270
+ await access(srcDir, constants.F_OK);
271
+ } catch {
272
+ spinner.warn(chalk.yellow('Bizar plugin source not found — skipping'));
273
+ return { copied: 0, errors: ['Source not found: ' + srcDir] };
274
+ }
275
+
276
+ const destDir = join(projectRoot, '.opencode', 'plugins', 'bizar');
277
+ await mkdir(destDir, { recursive: true });
278
+
279
+ // Exclude patterns per spec §9.2
280
+ const isExcluded = (entry) => {
281
+ const parts = entry.split('/');
282
+ return parts.some(part =>
283
+ part === 'node_modules' ||
284
+ part === 'dist' ||
285
+ part === '.DS_Store' ||
286
+ part.endsWith('.log')
287
+ );
288
+ };
289
+
290
+ const files = await readdirRecursive(srcDir);
291
+ const errors = [];
292
+ let copied = 0;
293
+
294
+ for (const file of files) {
295
+ if (isExcluded(file)) continue;
296
+ const src = join(srcDir, file);
297
+ const dst = join(destDir, file);
298
+ const dstParent = dirname(dst);
299
+ try {
300
+ await mkdir(dstParent, { recursive: true });
301
+ await copyFile(src, dst);
302
+ copied++;
303
+ } catch (err) {
304
+ errors.push(`Failed to copy ${file}: ${err.message}`);
305
+ }
306
+ }
307
+
308
+ if (errors.length === 0) {
309
+ spinner.succeed(chalk.green(`Installed Bizar plugin (${copied} files)`));
310
+ } else {
311
+ spinner.warn(chalk.yellow(`Installed Bizar plugin (${copied} files, ${errors.length} errors)`));
312
+ }
313
+ return { copied, errors };
314
+ }
315
+
316
+ export async function installRtk() {
317
+ const { execSync } = await import('node:child_process');
318
+
319
+ const already = await detectRtk();
320
+ if (already) {
321
+ const spinner = ora({ text: 'Configuring RTK for opencode...', color: 'magenta' }).start();
322
+ try {
323
+ execSync('rtk init -g --opencode', { stdio: 'pipe' });
324
+ spinner.succeed(chalk.green('RTK configured for opencode'));
325
+ } catch {
326
+ spinner.warn(chalk.yellow('Could not auto-configure RTK — run `rtk init -g --opencode` manually'));
327
+ }
328
+ return true;
329
+ }
330
+
331
+ const spinner = ora({ text: 'Installing RTK (Rust Token Killer)...', color: 'magenta' }).start();
332
+
333
+ if (process.platform === 'win32') {
334
+ spinner.fail(chalk.red('Automatic RTK install not supported on Windows. Install manually from https://github.com/rtk-ai/rtk'));
335
+ return false;
336
+ }
337
+
338
+ try {
339
+ execSync(
340
+ 'curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh',
341
+ { stdio: 'pipe', timeout: 60000 },
342
+ );
343
+ spinner.text = 'Configuring RTK for opencode...';
344
+ execSync('rtk init -g --opencode', { stdio: 'pipe' });
345
+ spinner.succeed(chalk.green('RTK installed and configured for opencode'));
346
+ return true;
347
+ } catch {
348
+ spinner.fail(chalk.red('RTK install failed. Install manually from https://github.com/rtk-ai/rtk'));
349
+ return false;
350
+ }
351
+ }
352
+
353
+ export async function installSemble() {
354
+ const { execSync } = await import('node:child_process');
355
+
356
+ const already = await detectSemble();
357
+ if (already) {
358
+ const spinner = ora({ text: 'Checking Semble code search...', color: 'cyan' }).start();
359
+ spinner.succeed(chalk.green('Semble ready'));
360
+ return true;
361
+ }
362
+
363
+ const spinner = ora({ text: 'Setting up Semble (code search)...', color: 'cyan' }).start();
364
+
365
+ const hasUv = await detectUv();
366
+ if (!hasUv) {
367
+ spinner.text = 'Installing uv (Python package manager)...';
368
+ try {
369
+ if (process.platform === 'win32') {
370
+ spinner.fail(chalk.red('Automatic uv install not supported on Windows. Install from https://docs.astral.sh/uv'));
371
+ return false;
372
+ }
373
+ execSync(
374
+ 'curl -LsSf https://astral.sh/uv/install.sh | sh',
375
+ { stdio: 'pipe', timeout: 60000 },
376
+ );
377
+ spinner.text = 'Setting up Semble...';
378
+ } catch {
379
+ spinner.fail(chalk.red('uv install failed. Install manually from https://docs.astral.sh/uv'));
380
+ return false;
381
+ }
382
+ }
383
+
384
+ try {
385
+ execSync('uv tool install "semble[mcp]"', { stdio: 'pipe', timeout: 60000 });
386
+ spinner.succeed(chalk.green('Semble installed (code search)'));
387
+ return true;
388
+ } catch {
389
+ spinner.warn(chalk.yellow('Semble install failed. Run `uv tool install "semble[mcp]"` manually'));
390
+ return false;
391
+ }
392
+ }
393
+
394
+ export async function installSkillsCli() {
395
+ const { execSync } = await import('node:child_process');
396
+
397
+ const already = await detectSkillsCli();
398
+ if (already) {
399
+ const spinner = ora({ text: 'Checking Skills CLI...', color: 'yellow' }).start();
400
+ spinner.succeed(chalk.green('Skills CLI ready'));
401
+ return true;
402
+ }
403
+
404
+ const spinner = ora({ text: 'Installing Skills CLI (skill discovery)...', color: 'yellow' }).start();
405
+
406
+ try {
407
+ execSync('npm install -g skills', { stdio: 'pipe', timeout: 30000 });
408
+ spinner.succeed(chalk.green('Skills CLI installed'));
409
+ return true;
410
+ } catch {
411
+ spinner.warn(chalk.yellow('Skills CLI install failed. Run `npm install -g skills` manually'));
412
+ return false;
413
+ }
414
+ }
415
+
416
+ export const SKILL_PACKS = {
417
+ core: {
418
+ label: 'Core — find-skills, skill-creator, write-a-skill',
419
+ repos: ['vercel-labs/skills'],
420
+ },
421
+ frontend: {
422
+ label: 'Frontend — React, web-design, composition, a11y, shadcn/ui',
423
+ repos: ['vercel-labs/agent-skills', 'shadcn/ui'],
424
+ },
425
+ backend: {
426
+ label: 'Backend — Supabase, Postgres, API patterns, auth',
427
+ repos: ['supabase/agent-skills'],
428
+ },
429
+ testing: {
430
+ label: 'Testing — TDD, E2E, Playwright, test patterns',
431
+ repos: ['mattpocock/skills', 'microsoft/playwright-cli'],
432
+ },
433
+ design: {
434
+ label: 'Design — frontend-design, UI/UX, taste skills',
435
+ repos: ['anthropics/skills', 'leonxlnx/taste-skill'],
436
+ },
437
+ };
438
+
439
+ export async function installCuratedSkills(packs) {
440
+ const { execSync } = await import('node:child_process');
441
+
442
+ const hasCli = await detectSkillsCli();
443
+ if (!hasCli) {
444
+ console.log(chalk.yellow(' ⚠ Skills CLI not available — skipping skill install'));
445
+ return false;
446
+ }
447
+
448
+ let total = 0;
449
+ for (const key of packs) {
450
+ const pack = SKILL_PACKS[key];
451
+ if (!pack) continue;
452
+
453
+ const spinner = ora({ text: `Installing ${key} skills...`, color: 'yellow' }).start();
454
+ for (const repo of pack.repos) {
455
+ try {
456
+ execSync(`skills add ${repo} --all -y`, { stdio: 'pipe', timeout: 60000 });
457
+ total++;
458
+ } catch {
459
+ spinner.warn(chalk.yellow(` ${repo} — some skills skipped or already installed`));
460
+ }
461
+ }
462
+ spinner.succeed(chalk.green(`${key}: ${pack.label}`));
463
+ }
464
+
465
+ if (total > 0) {
466
+ console.log(chalk.dim(` Installed from ${total} skill repositories`));
467
+ }
468
+ return true;
469
+ }
470
+
471
+ export async function installRules() {
472
+ const src = repoPath('config', 'rules');
473
+ const dest = join(opencodeConfigDir(), 'rules');
474
+ const { mkdirSync, readdirSync, copyFileSync } = await import('node:fs');
475
+ mkdirSync(dest, { recursive: true });
476
+ let count = 0;
477
+ for (const file of readdirSync(src).filter(f => f.endsWith('.md'))) {
478
+ copyFileSync(join(src, file), join(dest, file));
479
+ count++;
480
+ }
481
+ return count;
482
+ }
483
+
484
+ export async function installHooks() {
485
+ const src = repoPath('config', 'hooks');
486
+ const dest = join(opencodeConfigDir(), 'hooks');
487
+ const { mkdirSync, readdirSync, copyFileSync } = await import('node:fs');
488
+ mkdirSync(dest, { recursive: true });
489
+ let count = 0;
490
+ for (const file of readdirSync(src).filter(f => f.endsWith('.md'))) {
491
+ copyFileSync(join(src, file), join(dest, file));
492
+ count++;
493
+ }
494
+ return count;
495
+ }
496
+
497
+ export async function installCommands() {
498
+ const src = repoPath('config', 'commands');
499
+ const dest = join(opencodeConfigDir(), 'commands');
500
+ const { mkdirSync, readdirSync, copyFileSync } = await import('node:fs');
501
+ mkdirSync(dest, { recursive: true });
502
+ let count = 0;
503
+ for (const file of readdirSync(src).filter(f => f.endsWith('.md'))) {
504
+ copyFileSync(join(src, file), join(dest, file));
505
+ count++;
506
+ }
507
+ return count;
508
+ }