@polderlabs/bizar 3.12.2 → 3.12.4
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/doctor.mjs +12 -2
- package/cli/doctor.test.mjs +25 -0
- package/cli/install.mjs +95 -11
- package/cli/install.test.mjs +246 -0
- package/config/AGENTS.md +14 -0
- package/config/commands/audit.md +4 -0
- package/config/commands/bizar.md +4 -0
- package/config/commands/explain.md +4 -0
- package/config/commands/init.md +4 -0
- package/config/commands/learn.md +4 -0
- package/config/commands/plan.md +4 -0
- package/config/commands/pr-review.md +4 -0
- package/config/commands/tailscale-serve.md +1 -0
- package/config/commands/visual-plan.md +4 -0
- package/package.json +1 -1
package/cli/doctor.mjs
CHANGED
|
@@ -92,6 +92,10 @@ async function checkPluginEntryPresent() {
|
|
|
92
92
|
}
|
|
93
93
|
const hasBizar = plugins.some((p) => {
|
|
94
94
|
if (typeof p === 'string') return p.includes('bizar');
|
|
95
|
+
if (Array.isArray(p)) {
|
|
96
|
+
const [path] = p;
|
|
97
|
+
return typeof path === 'string' && path.includes('bizar');
|
|
98
|
+
}
|
|
95
99
|
if (p && typeof p === 'object') {
|
|
96
100
|
return (
|
|
97
101
|
(p.name || '').includes('bizar') ||
|
|
@@ -112,8 +116,14 @@ async function checkPluginPathResolves() {
|
|
|
112
116
|
const plugins = Array.isArray(cfg.plugin) ? cfg.plugin : [];
|
|
113
117
|
let lastChecked = null;
|
|
114
118
|
for (const p of plugins) {
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
let entryPath = null;
|
|
120
|
+
if (Array.isArray(p)) {
|
|
121
|
+
const [path] = p;
|
|
122
|
+
if (typeof path === 'string') entryPath = path;
|
|
123
|
+
} else if (p && typeof p === 'object' && p.path) {
|
|
124
|
+
entryPath = p.path;
|
|
125
|
+
}
|
|
126
|
+
if (!entryPath) continue;
|
|
117
127
|
const isAbs = entryPath.startsWith('/') || /^[a-z]:[\\/]/i.test(entryPath);
|
|
118
128
|
const resolved = isAbs ? entryPath : join(opencodeConfigDir(), entryPath);
|
|
119
129
|
lastChecked = resolved;
|
package/cli/doctor.test.mjs
CHANGED
|
@@ -254,6 +254,31 @@ describe('runDoctor() with fixture HOME', () => {
|
|
|
254
254
|
assert.equal(r.ok, true, r.message);
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
test('plugin-entry-present passes when plugin[] has [path, options] tuple', async () => {
|
|
258
|
+
writeOpencodeConfig({ plugin: [['./plugins/bizar/index.ts', { loopThresholdWarn: 5 }]] });
|
|
259
|
+
const result = await runDoctor({ silent: true });
|
|
260
|
+
const r = findCheck(result, 'plugin-entry-present');
|
|
261
|
+
assert.equal(r.ok, true, r.message);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('plugin-entry-present fails when tuple path does not contain "bizar"', async () => {
|
|
265
|
+
writeOpencodeConfig({ plugin: [['./plugins/other/index.ts', {}]] });
|
|
266
|
+
const result = await runDoctor({ silent: true });
|
|
267
|
+
const r = findCheck(result, 'plugin-entry-present');
|
|
268
|
+
assert.equal(r.ok, false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('plugin-path-resolves passes when tuple path resolves', async () => {
|
|
272
|
+
const home = process.env.HOME;
|
|
273
|
+
const pluginsDir = join(home, '.config', 'opencode', 'plugins', 'bizar');
|
|
274
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
275
|
+
writeFileSync(join(pluginsDir, 'index.ts'), '// fake plugin\n', 'utf8');
|
|
276
|
+
writeOpencodeConfig({ plugin: [['./plugins/bizar/index.ts', {}]] });
|
|
277
|
+
const result = await runDoctor({ silent: true });
|
|
278
|
+
const r = findCheck(result, 'plugin-path-resolves');
|
|
279
|
+
assert.equal(r.ok, true, r.message);
|
|
280
|
+
});
|
|
281
|
+
|
|
257
282
|
test('agent-files-installed fails when core agents missing', async () => {
|
|
258
283
|
writeOpencodeConfig({});
|
|
259
284
|
writeAgents('odin.md'); // missing quick, thor, tyr
|
package/cli/install.mjs
CHANGED
|
@@ -32,9 +32,22 @@ const AGENT_FILES = [
|
|
|
32
32
|
* breaking the dev workflow. Pass `{ force: true }` to override (after
|
|
33
33
|
* confirming the user really wants the deployed copy back).
|
|
34
34
|
*
|
|
35
|
+
* node_modules copy: the plugin imports `@polderlabs/bizar-sdk`, a
|
|
36
|
+
* workspace-internal package not on the public registry. The npm source
|
|
37
|
+
* bundles the SDK into its own `node_modules/`. After copying the plugin
|
|
38
|
+
* files, this function ALSO copies the source's `node_modules/` to the
|
|
39
|
+
* deployed `node_modules/` so Bun can resolve the import when the plugin
|
|
40
|
+
* is loaded from `~/.config/opencode/plugins/bizar/`. Without this, the
|
|
41
|
+
* plugin silently fails to load.
|
|
42
|
+
*
|
|
35
43
|
* Pass `{ silent: true }` to suppress the warning prints; the function
|
|
36
44
|
* still returns `true` if the dest is already in the desired state.
|
|
37
45
|
*
|
|
46
|
+
* Pass `{ sourceDir: '/path/to/fake' }` (test seam) to bypass the
|
|
47
|
+
* `npm root -g` lookup and use a caller-supplied source path. Used by
|
|
48
|
+
* cli/install.test.mjs to drive the function against a controlled
|
|
49
|
+
* filesystem without touching the real global npm root.
|
|
50
|
+
*
|
|
38
51
|
* Returns `true` if the plugin was installed (or already in place),
|
|
39
52
|
* `false` otherwise. Never throws.
|
|
40
53
|
*/
|
|
@@ -76,17 +89,24 @@ export async function installPluginFromGlobal(opts = {}) {
|
|
|
76
89
|
rmSync(destDir, { force: true });
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
// ── Resolve source path ─────────────────────────────────────────────────────
|
|
93
|
+
// Production: `npm root -g` + `@polderlabs/bizar-plugin`. Tests can pass
|
|
94
|
+
// `opts.sourceDir` to bypass the lookup entirely.
|
|
95
|
+
let pluginPath;
|
|
96
|
+
if (opts.sourceDir) {
|
|
97
|
+
pluginPath = opts.sourceDir;
|
|
98
|
+
} else {
|
|
99
|
+
let globalRoot;
|
|
100
|
+
try {
|
|
101
|
+
globalRoot = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
102
|
+
.toString()
|
|
103
|
+
.trim();
|
|
104
|
+
} catch {
|
|
105
|
+
console.log(chalk.yellow(' ⚠ Could not determine npm global root — skipping plugin install'));
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
pluginPath = join(globalRoot, '@polderlabs', 'bizar-plugin');
|
|
87
109
|
}
|
|
88
|
-
|
|
89
|
-
const pluginPath = join(globalRoot, '@polderlabs', 'bizar-plugin');
|
|
90
110
|
if (!existsSync(pluginPath)) {
|
|
91
111
|
console.log(chalk.dim(' ℹ Bizar plugin not installed globally. To install it:'));
|
|
92
112
|
console.log(chalk.dim(' npm install -g @polderlabs/bizar-plugin'));
|
|
@@ -115,11 +135,75 @@ export async function installPluginFromGlobal(opts = {}) {
|
|
|
115
135
|
await copyRecursive(pluginPath, destDir);
|
|
116
136
|
console.log(chalk.green(` ✓ Bizar plugin installed from global package`));
|
|
117
137
|
console.log(chalk.dim(` ${pluginPath} → ${destDir}`));
|
|
118
|
-
return true;
|
|
119
138
|
} catch (err) {
|
|
120
139
|
console.log(chalk.yellow(` ⚠ Failed to copy Bizar plugin: ${err.message}`));
|
|
121
140
|
return false;
|
|
122
141
|
}
|
|
142
|
+
|
|
143
|
+
// ── node_modules copy ───────────────────────────────────────────────────────
|
|
144
|
+
// The plugin imports `@polderlabs/bizar-sdk`, a workspace-internal package
|
|
145
|
+
// not on the public registry. The npm source bundles the SDK into its
|
|
146
|
+
// own `node_modules/`, so loading from `$(npm root -g)/...` resolves
|
|
147
|
+
// correctly. The deployed copy at `~/.config/opencode/plugins/bizar/`
|
|
148
|
+
// has no `node_modules`, so Bun can't resolve the import — without this
|
|
149
|
+
// copy the plugin silently fails to load. This used to be papered over
|
|
150
|
+
// by a manual `cp -r $(npm root -g)/.../node_modules ~/.config/...` step
|
|
151
|
+
// that got wiped every time `bizar update` re-ran the outer copy. Doing
|
|
152
|
+
// it here makes the fix durable across updates.
|
|
153
|
+
//
|
|
154
|
+
// Semantics: mirror `cp -r` — merge into dest if it exists as a real
|
|
155
|
+
// directory (overwrite same-named files, don't delete extras), error
|
|
156
|
+
// if dest is a symlink (don't dereference through it). Idempotent —
|
|
157
|
+
// running twice never breaks; files whose contents already match are
|
|
158
|
+
// rewritten byte-identically.
|
|
159
|
+
try {
|
|
160
|
+
const srcNmDir = join(pluginPath, 'node_modules');
|
|
161
|
+
if (existsSync(srcNmDir)) {
|
|
162
|
+
const dstNmDir = join(destDir, 'node_modules');
|
|
163
|
+
let dstIsSymlink = false;
|
|
164
|
+
try {
|
|
165
|
+
dstIsSymlink = lstatSync(dstNmDir).isSymbolicLink();
|
|
166
|
+
} catch {
|
|
167
|
+
// doesn't exist — fine, we'll create it below
|
|
168
|
+
}
|
|
169
|
+
if (dstIsSymlink) {
|
|
170
|
+
console.log(
|
|
171
|
+
chalk.yellow(
|
|
172
|
+
` ⚠ ${dstNmDir} is a symlink — refusing to copy node_modules (remove manually).`,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
// Top-level entry count (matches `npm ls --depth=0`) — gives the
|
|
177
|
+
// user a sense of what was bundled. e.g. "copied node_modules
|
|
178
|
+
// (25 packages) to deployed plugin".
|
|
179
|
+
const srcEntries = await readdir(srcNmDir, { withFileTypes: true });
|
|
180
|
+
await mkdir(dstNmDir, { recursive: true });
|
|
181
|
+
async function copyNodeModules(srcDir, dstDir) {
|
|
182
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const src = join(srcDir, entry.name);
|
|
185
|
+
const dst = join(dstDir, entry.name);
|
|
186
|
+
if (entry.isDirectory()) {
|
|
187
|
+
await mkdir(dst, { recursive: true });
|
|
188
|
+
await copyNodeModules(src, dst);
|
|
189
|
+
} else {
|
|
190
|
+
await copyFile(src, dst);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await copyNodeModules(srcNmDir, dstNmDir);
|
|
195
|
+
console.log(
|
|
196
|
+
chalk.green(
|
|
197
|
+
` ✓ copied node_modules (${srcEntries.length} packages) to deployed plugin`,
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.log(chalk.yellow(` ⚠ Failed to copy node_modules: ${err.message}`));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true;
|
|
123
207
|
}
|
|
124
208
|
|
|
125
209
|
export async function runInstaller() {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/install.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for `installPluginFromGlobal()` in cli/install.mjs — specifically
|
|
5
|
+
* the node_modules copy step added in the "make the plugin-install fix
|
|
6
|
+
* durable" change.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: mock HOME so opencodeConfigDir() resolves inside a tmpdir,
|
|
9
|
+
* and pass `opts.sourceDir` to bypass the `npm root -g` lookup with a
|
|
10
|
+
* caller-supplied fake plugin source.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the HOME-mocking pattern used by cli/dev-link.test.mjs.
|
|
13
|
+
*/
|
|
14
|
+
import { test, describe, beforeEach, afterEach, after } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import {
|
|
17
|
+
mkdtempSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
lstatSync,
|
|
23
|
+
symlinkSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
const { installPluginFromGlobal } = await import('./install.mjs');
|
|
29
|
+
|
|
30
|
+
const ORIG_HOME = process.env.HOME;
|
|
31
|
+
const ORIG_XDG = process.env.XDG_CONFIG_HOME;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Point HOME at a fresh tmpdir so the module's opencodeConfigDir()
|
|
35
|
+
* resolves inside it (via the fallback `<HOME>/.config/opencode`).
|
|
36
|
+
* Returns the tmpdir path.
|
|
37
|
+
*
|
|
38
|
+
* We deliberately do NOT set XDG_CONFIG_HOME. opencodeConfigDir() treats
|
|
39
|
+
* a set XDG_CONFIG_HOME as the direct parent (so `~/.config` →
|
|
40
|
+
* `~/.config/opencode`); setting it to a raw tmpdir would produce
|
|
41
|
+
* `<tmpdir>/opencode` instead of `<tmpdir>/.config/opencode` and break
|
|
42
|
+
* path alignment with the rest of the test.
|
|
43
|
+
*/
|
|
44
|
+
function freshHome() {
|
|
45
|
+
const home = mkdtempSync(join(tmpdir(), 'bizar-install-'));
|
|
46
|
+
process.env.HOME = home;
|
|
47
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
48
|
+
return home;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Path to the deployed plugin dir under the mocked HOME. */
|
|
52
|
+
function pluginDest(home) {
|
|
53
|
+
return join(home, '.config', 'opencode', 'plugins', 'bizar');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a fake plugin source tree, optionally including a `node_modules/`
|
|
58
|
+
* directory with a couple of packages. Used to drive the function under
|
|
59
|
+
* test without touching the real global npm install.
|
|
60
|
+
*/
|
|
61
|
+
function makeFakeSource({ withNodeModules = true } = {}) {
|
|
62
|
+
const src = mkdtempSync(join(tmpdir(), 'fake-plugin-src-'));
|
|
63
|
+
writeFileSync(
|
|
64
|
+
join(src, 'package.json'),
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
name: '@polderlabs/bizar-plugin',
|
|
67
|
+
version: '0.0.0-test',
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
writeFileSync(join(src, 'index.ts'), '// fake plugin');
|
|
71
|
+
if (withNodeModules) {
|
|
72
|
+
// Mimics the real npm install layout:
|
|
73
|
+
// node_modules/
|
|
74
|
+
// @polderlabs/bizar-sdk/package.json <-- the workspace-internal import
|
|
75
|
+
// some-dep/package.json <-- a transitive dep
|
|
76
|
+
// some-dep/lib/index.js <-- nested file
|
|
77
|
+
mkdirSync(join(src, 'node_modules'));
|
|
78
|
+
mkdirSync(join(src, 'node_modules', '@polderlabs'));
|
|
79
|
+
mkdirSync(join(src, 'node_modules', '@polderlabs', 'bizar-sdk'));
|
|
80
|
+
writeFileSync(
|
|
81
|
+
join(src, 'node_modules', '@polderlabs', 'bizar-sdk', 'package.json'),
|
|
82
|
+
JSON.stringify({ name: '@polderlabs/bizar-sdk', version: '1.0.0' }),
|
|
83
|
+
);
|
|
84
|
+
mkdirSync(join(src, 'node_modules', 'some-dep'));
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(src, 'node_modules', 'some-dep', 'package.json'),
|
|
87
|
+
JSON.stringify({ name: 'some-dep', version: '2.0.0' }),
|
|
88
|
+
);
|
|
89
|
+
mkdirSync(join(src, 'node_modules', 'some-dep', 'lib'));
|
|
90
|
+
writeFileSync(
|
|
91
|
+
join(src, 'node_modules', 'some-dep', 'lib', 'index.js'),
|
|
92
|
+
'module.exports = 1;',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return src;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Restore env at the very end (individual tests restore in afterEach). */
|
|
99
|
+
after(() => {
|
|
100
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
101
|
+
else process.env.HOME = ORIG_HOME;
|
|
102
|
+
if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
103
|
+
else process.env.XDG_CONFIG_HOME = ORIG_XDG;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── installPluginFromGlobal — node_modules copy ───────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe('installPluginFromGlobal() — node_modules copy', () => {
|
|
109
|
+
let home;
|
|
110
|
+
let sourceDir;
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
home = freshHome();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
if (home && existsSync(home)) rmSync(home, { recursive: true, force: true });
|
|
118
|
+
if (sourceDir && existsSync(sourceDir)) rmSync(sourceDir, { recursive: true, force: true });
|
|
119
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
120
|
+
else process.env.HOME = ORIG_HOME;
|
|
121
|
+
if (ORIG_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
122
|
+
else process.env.XDG_CONFIG_HOME = ORIG_XDG;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('copies node_modules from source when present', async () => {
|
|
126
|
+
sourceDir = makeFakeSource({ withNodeModules: true });
|
|
127
|
+
const dest = pluginDest(home);
|
|
128
|
+
|
|
129
|
+
const ok = await installPluginFromGlobal({ sourceDir });
|
|
130
|
+
assert.equal(ok, true);
|
|
131
|
+
|
|
132
|
+
// Plugin file copied.
|
|
133
|
+
assert.equal(existsSync(join(dest, 'index.ts')), true);
|
|
134
|
+
assert.equal(existsSync(join(dest, 'package.json')), true);
|
|
135
|
+
|
|
136
|
+
// node_modules copied (the actual fix).
|
|
137
|
+
const destNm = join(dest, 'node_modules');
|
|
138
|
+
assert.equal(existsSync(destNm), true);
|
|
139
|
+
assert.equal(
|
|
140
|
+
existsSync(join(destNm, 'some-dep', 'package.json')),
|
|
141
|
+
true,
|
|
142
|
+
'top-level dep copied',
|
|
143
|
+
);
|
|
144
|
+
assert.equal(
|
|
145
|
+
existsSync(join(destNm, 'some-dep', 'lib', 'index.js')),
|
|
146
|
+
true,
|
|
147
|
+
'nested file inside top-level dep copied',
|
|
148
|
+
);
|
|
149
|
+
assert.equal(
|
|
150
|
+
existsSync(join(destNm, '@polderlabs', 'bizar-sdk', 'package.json')),
|
|
151
|
+
true,
|
|
152
|
+
'scoped @polderlabs/bizar-sdk copied (the actual bug fix)',
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('no-op for node_modules when source has none', async () => {
|
|
157
|
+
sourceDir = makeFakeSource({ withNodeModules: false });
|
|
158
|
+
const dest = pluginDest(home);
|
|
159
|
+
|
|
160
|
+
const ok = await installPluginFromGlobal({ sourceDir });
|
|
161
|
+
assert.equal(ok, true);
|
|
162
|
+
|
|
163
|
+
// Plugin file copied.
|
|
164
|
+
assert.equal(existsSync(join(dest, 'index.ts')), true);
|
|
165
|
+
|
|
166
|
+
// node_modules should NOT exist — we don't create empty dirs.
|
|
167
|
+
const destNm = join(dest, 'node_modules');
|
|
168
|
+
assert.equal(existsSync(destNm), false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('idempotent: running twice does not error and preserves extras', async () => {
|
|
172
|
+
sourceDir = makeFakeSource({ withNodeModules: true });
|
|
173
|
+
const dest = pluginDest(home);
|
|
174
|
+
|
|
175
|
+
const ok1 = await installPluginFromGlobal({ sourceDir });
|
|
176
|
+
assert.equal(ok1, true);
|
|
177
|
+
|
|
178
|
+
// Drop a marker that simulates a user adding their own package.
|
|
179
|
+
// After a second run, this marker must STILL be present (cp -r
|
|
180
|
+
// semantics — don't delete, just overwrite/add).
|
|
181
|
+
const destNm = join(dest, 'node_modules');
|
|
182
|
+
mkdirSync(join(destNm, 'user-added'), { recursive: true });
|
|
183
|
+
writeFileSync(join(destNm, 'user-added', 'marker.json'), '{}');
|
|
184
|
+
|
|
185
|
+
const ok2 = await installPluginFromGlobal({ sourceDir });
|
|
186
|
+
assert.equal(ok2, true, 'second call should still return true');
|
|
187
|
+
|
|
188
|
+
// User's marker is still there.
|
|
189
|
+
assert.equal(
|
|
190
|
+
existsSync(join(destNm, 'user-added', 'marker.json')),
|
|
191
|
+
true,
|
|
192
|
+
'user-added extras preserved across re-runs',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// And the npm-bundled contents are still there.
|
|
196
|
+
assert.equal(
|
|
197
|
+
existsSync(join(destNm, '@polderlabs', 'bizar-sdk', 'package.json')),
|
|
198
|
+
true,
|
|
199
|
+
'@polderlabs/bizar-sdk still present after second run',
|
|
200
|
+
);
|
|
201
|
+
assert.equal(
|
|
202
|
+
existsSync(join(destNm, 'some-dep', 'lib', 'index.js')),
|
|
203
|
+
true,
|
|
204
|
+
'nested file still present after second run',
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('refuses to overwrite a symlinked node_modules', async () => {
|
|
209
|
+
sourceDir = makeFakeSource({ withNodeModules: true });
|
|
210
|
+
const dest = pluginDest(home);
|
|
211
|
+
|
|
212
|
+
// Pre-create dest as a real dir (the outer copy needs a real dest)
|
|
213
|
+
// AND pre-create dest/node_modules as a symlink to some unrelated
|
|
214
|
+
// location. The fix must NOT dereference through the symlink and
|
|
215
|
+
// must leave it intact.
|
|
216
|
+
mkdirSync(dest, { recursive: true });
|
|
217
|
+
const linkTarget = mkdtempSync(join(tmpdir(), 'nm-link-target-'));
|
|
218
|
+
const destNm = join(dest, 'node_modules');
|
|
219
|
+
symlinkSync(linkTarget, destNm);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const ok = await installPluginFromGlobal({ sourceDir });
|
|
223
|
+
// Outer install succeeds (the file copy still works — it doesn't
|
|
224
|
+
// touch node_modules at the dest).
|
|
225
|
+
assert.equal(ok, true);
|
|
226
|
+
|
|
227
|
+
// The symlink must still be a symlink — we refused to dereference.
|
|
228
|
+
assert.equal(
|
|
229
|
+
lstatSync(destNm).isSymbolicLink(),
|
|
230
|
+
true,
|
|
231
|
+
'node_modules symlink preserved (not dereferenced)',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// SDK NOT copied into the link target (we skipped the copy).
|
|
235
|
+
assert.equal(
|
|
236
|
+
existsSync(join(linkTarget, '@polderlabs')),
|
|
237
|
+
false,
|
|
238
|
+
'refused copy did not leak into link target',
|
|
239
|
+
);
|
|
240
|
+
} finally {
|
|
241
|
+
rmSync(linkTarget, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
console.log(' install.mjs tests loaded — run with: node --test cli/install.test.mjs');
|
package/config/AGENTS.md
CHANGED
|
@@ -347,6 +347,20 @@ This section is the single source of truth for every Bizar agent's behavior. It
|
|
|
347
347
|
> | `task` (subagent dispatch) | `task` — same — used by Odin to dispatch subagents |
|
|
348
348
|
> | MCP servers | `semble` (codebase search), `hindsight` (memory), and any user-added servers in `config/opencode.json` |
|
|
349
349
|
|
|
350
|
+
### Simplicity Rule — do not overcomplicate
|
|
351
|
+
|
|
352
|
+
**This is the most important rule in this baseline. Every agent, every time, no exceptions.**
|
|
353
|
+
|
|
354
|
+
- **Match the work to the ask.** If the user asked one question, answer one question. If they asked for one change, make one change. Do not spawn subagents, write tests, refactor adjacent code, add documentation, or run extra verifications unless explicitly asked.
|
|
355
|
+
- **No speculative features.** Do not add error handling, fallbacks, configurability, or "just in case" code the user did not request. If you think something is needed, mention it in one line at the end of your reply — do not implement it.
|
|
356
|
+
- **No speculative questions.** If the request is clear enough to act, act. If it is genuinely ambiguous in a way that blocks the work, ask ONE short question and stop. Do not list three options, do not write a decision matrix, do not draft both versions.
|
|
357
|
+
- **No over-explanation.** Short answer for a short question. The first sentence should contain the outcome. Skip preamble ("Great question!"), skip postamble ("Let me know if you need anything else!"), skip recap. The work is the work; the words around it are noise.
|
|
358
|
+
- **Tools only when they earn their keep.** A tool call that returns nothing the user wanted is a waste. If you can answer from context, answer from context. If you must search, search once and decisively.
|
|
359
|
+
- **Subagents are expensive.** Delegating to a subagent costs 5–30 seconds and several model calls. Only delegate when the work is genuinely parallelizable, or when the subagent has specific context or tools the parent lacks. A single agent doing the work end-to-end is almost always faster than a fan-out for tasks under a few hundred lines.
|
|
360
|
+
- **Short replies are good replies.** "Done." "Fixed." "The bug was X." A short, correct answer beats a long, hedging one. If the user wants depth, they will ask.
|
|
361
|
+
|
|
362
|
+
**When in doubt: do the smallest thing that solves the actual problem, then stop.**
|
|
363
|
+
|
|
350
364
|
### Identity preamble
|
|
351
365
|
|
|
352
366
|
- Bizar is a Norse-pantheon multi-agent system for opencode. Odin is the default primary agent; Frigg, Vör, Mimir, Heimdall, Hermod, Thor, Baldr, Tyr, Vidarr, and Forseti are the subagents.
|
package/config/commands/audit.md
CHANGED
package/config/commands/bizar.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Bizar Plugin Menu — route to the right Bizar action based on the user's request.
|
|
3
|
+
agent: odin
|
|
4
|
+
---
|
|
1
5
|
# Bizar Dashboard
|
|
2
6
|
|
|
3
7
|
The `/bizar` command launches the Bizar dashboard in your browser — a fully integrated workspace with Overview, Chat, Agents, Plans, Projects, Config, and Settings panels. The dashboard binds to `127.0.0.1` only and runs as a local Express + WebSocket server on a free port (preferred: 4321).
|
package/config/commands/init.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run bizar init to detect project stack, install relevant skills, and create .bizar/PROJECT.md.
|
|
3
|
+
agent: heimdall
|
|
4
|
+
---
|
|
1
5
|
Run `bizar init` from the project root to:
|
|
2
6
|
1. Detect the project stack (language, framework, database, tools)
|
|
3
7
|
2. Install relevant skills from the opencode skills registry
|
package/config/commands/learn.md
CHANGED
package/config/commands/plan.md
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route to @hermod for PR review mode. Launches @mimir (research) and @forseti (audit) in parallel, then posts the combined review as a PR comment.
|
|
3
|
+
agent: hermod
|
|
4
|
+
---
|
|
1
5
|
Route to @hermod for PR review mode. Launches @mimir (research) and @forseti (audit) in parallel, then posts the combined review as a PR comment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.4",
|
|
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|