@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.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/audit.mjs +144 -0
- package/cli/banner.mjs +41 -0
- package/cli/bin.mjs +186 -0
- package/cli/copy.mjs +508 -0
- package/cli/export.mjs +87 -0
- package/cli/init.mjs +147 -0
- package/cli/install.mjs +390 -0
- package/cli/plan-templates.mjs +523 -0
- package/cli/plan.mjs +2087 -0
- package/cli/prompts.mjs +163 -0
- package/cli/update.mjs +273 -0
- package/cli/utils.mjs +153 -0
- package/config/AGENTS.md +282 -0
- package/config/agents/baldr.md +148 -0
- package/config/agents/forseti.md +112 -0
- package/config/agents/frigg.md +101 -0
- package/config/agents/heimdall.md +157 -0
- package/config/agents/hermod.md +144 -0
- package/config/agents/mimir.md +115 -0
- package/config/agents/odin.md +309 -0
- package/config/agents/quick.md +78 -0
- package/config/agents/semble-search.md +44 -0
- package/config/agents/thor.md +97 -0
- package/config/agents/tyr.md +96 -0
- package/config/agents/vidarr.md +100 -0
- package/config/agents/vor.md +140 -0
- package/config/commands/audit.md +1 -0
- package/config/commands/explain.md +1 -0
- package/config/commands/init.md +1 -0
- package/config/commands/learn.md +1 -0
- package/config/commands/pr-review.md +1 -0
- package/config/commands/tailscale-serve.md +96 -0
- package/config/hooks/README.md +29 -0
- package/config/hooks/post-tool-use.md +16 -0
- package/config/hooks/pre-tool-use.md +16 -0
- package/config/opencode.json +52 -0
- package/config/opencode.json.template +52 -0
- package/config/rules/general.md +8 -0
- package/config/rules/git.md +11 -0
- package/config/rules/javascript.md +10 -0
- package/config/rules/python.md +10 -0
- package/config/rules/testing.md +10 -0
- package/config/skills/bizar/README.md +9 -0
- package/config/skills/bizar/SKILL.md +187 -0
- package/config/skills/cpp-coding-standards/README.md +28 -0
- package/config/skills/cpp-coding-standards/SKILL.md +634 -0
- package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
- package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/config/skills/cpp-testing/README.md +28 -0
- package/config/skills/cpp-testing/SKILL.md +304 -0
- package/config/skills/cpp-testing/agents/openai.yaml +4 -0
- package/config/skills/cpp-testing/references/coverage.md +370 -0
- package/config/skills/cpp-testing/references/framework-compare.md +175 -0
- package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/config/skills/cpp-testing/references/mocking.md +364 -0
- package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/config/skills/embedded-esp-idf/README.md +41 -0
- package/config/skills/embedded-esp-idf/SKILL.md +439 -0
- package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
- package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/config/skills/self-improvement/SKILL.md +64 -0
- package/package.json +47 -0
- package/templates/plan/htmx.min.js +1 -0
- package/templates/plan/library/bug-investigation.mdx +79 -0
- package/templates/plan/library/decision-record.mdx +71 -0
- package/templates/plan/library/feature-design.mdx +92 -0
- package/templates/plan/meta.json.template +8 -0
- package/templates/plan/plan.canvas.template +1711 -0
- package/templates/plan/plan.html.template +937 -0
- 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
|
+
}
|