@polderlabs/bizar 6.0.2 → 6.2.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/bizar-dash/DESIGN.md +18 -23
- package/bizar-dash/critique.json +6 -6
- package/cli/bin.mjs +19 -1
- package/cli/commands/validate.mjs +439 -0
- package/cli/commands/validate.test.mjs +279 -0
- package/cli/install.mjs +17 -40
- package/cli/post-install-smoke.mjs +96 -3
- package/cli/post-install-smoke.test.mjs +3 -5
- package/cli/provision.mjs +97 -11
- package/cli/utils.mjs +9 -25
- package/config/cline.json.template +15 -0
- package/config/commands/team.md +124 -0
- package/config/commands/test.md +60 -0
- package/config/commands/validate.md +66 -0
- package/package.json +5 -4
- package/packages/sdk/package.json +1 -1
- package/plugins/bizar/package.json +1 -1
- package/plugins/bizar/src/clineruntime.ts +1 -1
- package/plugins/bizar/tests/clineruntime-config.test.ts +44 -0
- package/scripts/bh-full-e2e.mjs +389 -0
- package/scripts/check-arch.sh +80 -0
- package/scripts/check-deps.mjs +510 -0
- package/scripts/clean-state-check.sh +81 -0
- package/scripts/git-hooks/pre-commit +59 -0
- package/scripts/git-hooks/pre-push +54 -0
- package/scripts/install-hooks.sh +29 -0
- package/scripts/install-service.mjs +190 -0
- package/scripts/session-trace.sh +52 -0
- package/scripts/verify-feature.sh +102 -0
- package/bizar-dash/image-1.png +0 -0
- package/bizar-dash/image-2.png +0 -0
- package/bizar-dash/image-3.png +0 -0
- package/bizar-dash/image-4.png +0 -0
- package/bizar-dash/image-5.png +0 -0
- package/bizar-dash/image-6.png +0 -0
- package/bizar-dash/image-7.png +0 -0
- package/bizar-dash/image.png +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/validate.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.2.0 — Unit tests for the new `bizar validate` subcommand.
|
|
5
|
+
*
|
|
6
|
+
* We exercise `runValidate()` directly. CLINE_DIR is mocked via
|
|
7
|
+
* `process.env.CLINE_DIR`; a minimal-but-complete Bizar install is
|
|
8
|
+
* staged inside a tmpdir. We then progressively delete files to
|
|
9
|
+
* verify each check fails with a sensible message.
|
|
10
|
+
*/
|
|
11
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { Writable } from 'node:stream';
|
|
17
|
+
|
|
18
|
+
const { runValidate } = await import('./validate.mjs');
|
|
19
|
+
|
|
20
|
+
const ORIG_CLINE_DIR = process.env.CLINE_DIR;
|
|
21
|
+
const ORIG_HOME = process.env.HOME;
|
|
22
|
+
|
|
23
|
+
let workDir;
|
|
24
|
+
|
|
25
|
+
function freshWorkDir() {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), 'bizar-validate-'));
|
|
27
|
+
process.env.CLINE_DIR = root;
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeFakeClineInstall(root) {
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(root, 'cline.json'),
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
$schema: 'https://docs.cline.bot/config.json',
|
|
36
|
+
plugin: [[
|
|
37
|
+
'./plugins/bizar',
|
|
38
|
+
{ loopThresholdWarn: 5, loopThresholdEscalate: 8, loopThresholdBlock: 12, loopWindowSize: 10 },
|
|
39
|
+
]],
|
|
40
|
+
default_agent: 'odin',
|
|
41
|
+
permission: 'allow',
|
|
42
|
+
snapshot: false,
|
|
43
|
+
instructions: ['.cline/instructions/bizar-tools.md'],
|
|
44
|
+
provider: {
|
|
45
|
+
'9router': {
|
|
46
|
+
baseUrl: 'http://localhost:20128/v1',
|
|
47
|
+
apiKey: '${env:NINEROUTER_KEY}',
|
|
48
|
+
models: { 'minimax/MiniMax-M3': { reasoning: true } },
|
|
49
|
+
},
|
|
50
|
+
minimax: { models: {} },
|
|
51
|
+
},
|
|
52
|
+
}, null, 2),
|
|
53
|
+
);
|
|
54
|
+
const agentsDir = join(root, 'agents');
|
|
55
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
56
|
+
for (const f of [
|
|
57
|
+
'odin.md', 'vor.md', 'frigg.md', 'quick.md',
|
|
58
|
+
'mimir.md', 'heimdall.md', 'hermod.md', 'thor.md', 'baldr.md',
|
|
59
|
+
'tyr.md', 'vidarr.md', 'forseti.md',
|
|
60
|
+
'semble-search.md', 'agent-browser.md',
|
|
61
|
+
]) {
|
|
62
|
+
writeFileSync(join(agentsDir, f), '# fake agent');
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(join(agentsDir, 'odin.yaml'), 'name: odin\ndescription: odin\n');
|
|
65
|
+
const cmdsDir = join(root, 'commands');
|
|
66
|
+
mkdirSync(cmdsDir, { recursive: true });
|
|
67
|
+
for (const f of [
|
|
68
|
+
'audit.md', 'bizar.md', 'explain.md', 'init.md', 'learn.md',
|
|
69
|
+
'plan.md', 'plow-through.md', 'pr-review.md', 'tailscale-serve.md',
|
|
70
|
+
'visual-plan.md',
|
|
71
|
+
'team.md', 'test.md', 'validate.md',
|
|
72
|
+
]) {
|
|
73
|
+
writeFileSync(join(cmdsDir, f), '# fake command');
|
|
74
|
+
}
|
|
75
|
+
const skillsDir = join(root, 'skills');
|
|
76
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
77
|
+
for (const skill of ['9router', 'bizar', 'obsidian']) {
|
|
78
|
+
mkdirSync(join(skillsDir, skill), { recursive: true });
|
|
79
|
+
writeFileSync(join(skillsDir, skill, 'SKILL.md'), '# fake skill');
|
|
80
|
+
}
|
|
81
|
+
const rulesDir = join(root, 'rules');
|
|
82
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
83
|
+
for (const r of ['general.md', 'git.md', 'javascript.md', 'python.md', 'testing.md', 'thinking.md', 'uncertainty.md']) {
|
|
84
|
+
writeFileSync(join(rulesDir, r), '# fake rule');
|
|
85
|
+
}
|
|
86
|
+
const hooksDir = join(root, 'hooks');
|
|
87
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
88
|
+
for (const h of ['pre-tool-use.md', 'post-tool-use.md', 'README.md']) {
|
|
89
|
+
writeFileSync(join(hooksDir, h), '# fake hook');
|
|
90
|
+
}
|
|
91
|
+
const pluginDir = join(root, 'plugins', 'bizar');
|
|
92
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
93
|
+
writeFileSync(join(pluginDir, 'index.ts'), '// fake plugin entry — >100 bytes to pass size check\n'.repeat(10));
|
|
94
|
+
mkdirSync(join(pluginDir, 'src'), { recursive: true });
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(pluginDir, 'src', 'clineruntime.ts'),
|
|
97
|
+
'export class ClineRuntime { config = { enableAgentTeams: true }; }\n',
|
|
98
|
+
);
|
|
99
|
+
// node_modules with the required runtime deps
|
|
100
|
+
const nmDir = join(pluginDir, 'node_modules');
|
|
101
|
+
mkdirSync(nmDir, { recursive: true });
|
|
102
|
+
mkdirSync(join(nmDir, 'zod'), { recursive: true });
|
|
103
|
+
writeFileSync(join(nmDir, 'zod', 'package.json'), '{}');
|
|
104
|
+
mkdirSync(join(nmDir, '@cline'), { recursive: true });
|
|
105
|
+
for (const dep of ['@cline/sdk', '@cline/core', '@cline/shared']) {
|
|
106
|
+
mkdirSync(join(nmDir, dep), { recursive: true });
|
|
107
|
+
writeFileSync(join(nmDir, dep, 'package.json'), '{}');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
workDir = freshWorkDir();
|
|
113
|
+
makeFakeClineInstall(workDir);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
|
|
118
|
+
if (ORIG_CLINE_DIR === undefined) delete process.env.CLINE_DIR;
|
|
119
|
+
else process.env.CLINE_DIR = ORIG_CLINE_DIR;
|
|
120
|
+
if (ORIG_HOME === undefined) delete process.env.HOME;
|
|
121
|
+
else process.env.HOME = ORIG_HOME;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Capture stdout + exit code for a single runValidate() call.
|
|
126
|
+
* The test runner writes its own progress to stdout, so we swap
|
|
127
|
+
* process.stdout for a sink Writable during the call.
|
|
128
|
+
*/
|
|
129
|
+
async function captureRun(opts) {
|
|
130
|
+
const captured = [];
|
|
131
|
+
const sink = new Writable({
|
|
132
|
+
write(chunk, _enc, cb) {
|
|
133
|
+
captured.push(Buffer.from(chunk));
|
|
134
|
+
cb();
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const origStdoutDescriptor = Object.getOwnPropertyDescriptor(process, 'stdout');
|
|
138
|
+
const origExit = process.exit;
|
|
139
|
+
let exitCode = null;
|
|
140
|
+
Object.defineProperty(process, 'stdout', { value: sink, configurable: true, writable: true });
|
|
141
|
+
process.exit = (code) => { exitCode = code; };
|
|
142
|
+
try {
|
|
143
|
+
await runValidate(opts);
|
|
144
|
+
} finally {
|
|
145
|
+
if (origStdoutDescriptor) Object.defineProperty(process, 'stdout', origStdoutDescriptor);
|
|
146
|
+
process.exit = origExit;
|
|
147
|
+
}
|
|
148
|
+
return { stdout: Buffer.concat(captured).toString('utf8'), exitCode };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
describe('runValidate() — JSON output', () => {
|
|
152
|
+
test('returns structured JSON with passed/failed/results', async () => {
|
|
153
|
+
const { stdout, exitCode } = await captureRun({ json: true });
|
|
154
|
+
assert.equal(exitCode, 0, 'should exit 0 on success');
|
|
155
|
+
const parsed = JSON.parse(stdout);
|
|
156
|
+
assert.equal(typeof parsed.passed, 'number');
|
|
157
|
+
assert.equal(typeof parsed.failed, 'number');
|
|
158
|
+
assert.ok(Array.isArray(parsed.results));
|
|
159
|
+
assert.ok(parsed.results.length >= 18, 'expected at least 18 checks');
|
|
160
|
+
for (const r of parsed.results) {
|
|
161
|
+
assert.ok(typeof r.name === 'string');
|
|
162
|
+
assert.ok(typeof r.ok === 'boolean');
|
|
163
|
+
assert.ok(typeof r.message === 'string');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('fails on missing team command', async () => {
|
|
168
|
+
rmSync(join(workDir, 'commands', 'team.md'));
|
|
169
|
+
const { exitCode } = await captureRun({ json: true });
|
|
170
|
+
assert.equal(exitCode, 1, 'should exit 1 on team-command-present failure');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('fails on missing test command', async () => {
|
|
174
|
+
rmSync(join(workDir, 'commands', 'test.md'));
|
|
175
|
+
const { exitCode } = await captureRun({ json: true });
|
|
176
|
+
assert.equal(exitCode, 1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('fails on missing validate command', async () => {
|
|
180
|
+
rmSync(join(workDir, 'commands', 'validate.md'));
|
|
181
|
+
const { exitCode } = await captureRun({ json: true });
|
|
182
|
+
assert.equal(exitCode, 1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('fails on missing agent file', async () => {
|
|
186
|
+
rmSync(join(workDir, 'agents', 'mimir.md'));
|
|
187
|
+
const { exitCode } = await captureRun({ json: true });
|
|
188
|
+
assert.equal(exitCode, 1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('fails when cline.json missing', async () => {
|
|
192
|
+
rmSync(join(workDir, 'cline.json'));
|
|
193
|
+
const { exitCode } = await captureRun({ json: true });
|
|
194
|
+
assert.equal(exitCode, 1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('fails when enableAgentTeams is false in clineruntime.ts', async () => {
|
|
198
|
+
writeFileSync(
|
|
199
|
+
join(workDir, 'plugins', 'bizar', 'src', 'clineruntime.ts'),
|
|
200
|
+
'export class ClineRuntime { config = { enableAgentTeams: false }; }\n',
|
|
201
|
+
);
|
|
202
|
+
const { exitCode } = await captureRun({ json: true });
|
|
203
|
+
assert.equal(exitCode, 1, 'should fail when enableAgentTeams is false');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('fails when provider config is missing', async () => {
|
|
207
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
208
|
+
delete cfg.provider;
|
|
209
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
210
|
+
const { exitCode } = await captureRun({ json: true });
|
|
211
|
+
assert.equal(exitCode, 1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('passes when only 9router is configured (no minimax)', async () => {
|
|
215
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
216
|
+
delete cfg.provider.minimax;
|
|
217
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
218
|
+
const { exitCode } = await captureRun({ json: true });
|
|
219
|
+
assert.equal(exitCode, 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('passes when only minimax is configured (no 9router)', async () => {
|
|
223
|
+
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
224
|
+
delete cfg.provider['9router'];
|
|
225
|
+
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
226
|
+
const { exitCode } = await captureRun({ json: true });
|
|
227
|
+
assert.equal(exitCode, 0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('default mode is lenient on 9router-unreachable', async () => {
|
|
231
|
+
const { stdout, exitCode } = await captureRun({ json: true });
|
|
232
|
+
const parsed = JSON.parse(stdout);
|
|
233
|
+
const nineR = parsed.results.find((r) => r.name === '9router-reachable');
|
|
234
|
+
assert.ok(nineR, '9router-reachable check should be present');
|
|
235
|
+
if (!nineR.ok) {
|
|
236
|
+
// 9router is not running. Default mode must be lenient.
|
|
237
|
+
assert.equal(exitCode, 0, 'default mode should be lenient when 9router is down');
|
|
238
|
+
} else {
|
|
239
|
+
// 9router is running (some test envs have it). Just confirm the
|
|
240
|
+
// check is in the list and exit code is sane.
|
|
241
|
+
assert.equal(exitCode, 0, 'expected exit 0 when all critical checks pass');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('--strict fails when 9router is unreachable', async () => {
|
|
246
|
+
// We can't easily kill 9router in a test, but we can prove that
|
|
247
|
+
// --strict mode would fail if 9router were down: set NINEROUTER_URL
|
|
248
|
+
// to a port nothing is listening on.
|
|
249
|
+
const origUrl = process.env.NINEROUTER_URL;
|
|
250
|
+
process.env.NINEROUTER_URL = 'http://127.0.0.1:1'; // nothing here
|
|
251
|
+
try {
|
|
252
|
+
const { exitCode } = await captureRun({ json: true, strict: true });
|
|
253
|
+
assert.equal(exitCode, 1, 'strict mode should fail when 9router is unreachable');
|
|
254
|
+
} finally {
|
|
255
|
+
if (origUrl === undefined) delete process.env.NINEROUTER_URL;
|
|
256
|
+
else process.env.NINEROUTER_URL = origUrl;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('--only filters to a single check', async () => {
|
|
261
|
+
const { stdout, exitCode } = await captureRun({ json: true, only: 'team-command-present' });
|
|
262
|
+
assert.equal(exitCode, 0);
|
|
263
|
+
const parsed = JSON.parse(stdout);
|
|
264
|
+
assert.equal(parsed.results.length, 1);
|
|
265
|
+
assert.equal(parsed.results[0].name, 'team-command-present');
|
|
266
|
+
assert.equal(parsed.results[0].ok, true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('--only on a missing file fails with exit 1', async () => {
|
|
270
|
+
rmSync(join(workDir, 'commands', 'team.md'));
|
|
271
|
+
const { exitCode } = await captureRun({ json: true, only: 'team-command-present' });
|
|
272
|
+
assert.equal(exitCode, 1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('--only with an unknown name exits 2', async () => {
|
|
276
|
+
const { exitCode } = await captureRun({ json: true, only: 'this-does-not-exist' });
|
|
277
|
+
assert.equal(exitCode, 2);
|
|
278
|
+
});
|
|
279
|
+
});
|
package/cli/install.mjs
CHANGED
|
@@ -8,9 +8,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
// `__dirname` is a CommonJS global. ESM modules don't have it. We define it
|
|
9
9
|
// at module scope so all functions in this file can use it without each
|
|
10
10
|
// having to recreate the polyfill. (v4.2.3 — previously only `runInstaller`
|
|
11
|
-
// had it, which caused `promptAndInstallOptional`
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// had it, which caused the legacy `promptAndInstallOptional` bootstrap
|
|
12
|
+
// to crash with `ERR_AMBIGUOUS_MODULE_SYNTAX`. v6.1.0 — that helper
|
|
13
|
+
// has been removed; `__dirname` is still used by `runPostInstall` for
|
|
14
|
+
// the same reason.)
|
|
14
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
|
|
16
17
|
import { showBanner, showPantheon, sectionHeading } from './banner.mjs';
|
|
@@ -302,39 +303,6 @@ async function isPackageInstalled(name) {
|
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
async function promptAndInstallOptional() {
|
|
306
|
-
// v4.4.5 — Plugin and dashboard are both shipped inside this package.
|
|
307
|
-
// No separate npm install step is required for either. The plugin
|
|
308
|
-
// copy happens via installPluginFromGlobal() in runInstaller(); the
|
|
309
|
-
// dashboard is loaded directly from this package's bizar-dash/src/.
|
|
310
|
-
// We just verify they're present here and warn loudly if not.
|
|
311
|
-
const pluginPath = join(__dirname, '..', 'plugins', 'bizar');
|
|
312
|
-
if (!existsSync(pluginPath)) {
|
|
313
|
-
console.error('');
|
|
314
|
-
console.error(' ✗ Plugin source not found at plugins/bizar/.');
|
|
315
|
-
console.error(' This package is missing the plugin source. Reinstall:');
|
|
316
|
-
console.error(' npm install -g @polderlabs/bizar --force');
|
|
317
|
-
} else {
|
|
318
|
-
console.log(' ✓ Plugin source present (plugins/bizar/)');
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const dashDir = join(__dirname, '..', 'bizar-dash');
|
|
322
|
-
// v6.0.2 — Dashboard shipped as a resource directory since v4.0.0.
|
|
323
|
-
// It no longer carries its own `package.json` (it became a separate
|
|
324
|
-
// `@polderlabs/bizar-dash` npm package, then was collapsed back in
|
|
325
|
-
// for v6.x). Probe for the built artifact + server source instead.
|
|
326
|
-
const dashDistHtml = join(dashDir, 'dist', 'index.html');
|
|
327
|
-
const dashServerSrc = join(dashDir, 'src', 'server', 'api.mjs');
|
|
328
|
-
if (!existsSync(dashDistHtml) && !existsSync(dashServerSrc)) {
|
|
329
|
-
console.error('');
|
|
330
|
-
console.error(' ✗ Dashboard source not found at bizar-dash/.');
|
|
331
|
-
console.error(' This package is missing the dashboard source. Reinstall:');
|
|
332
|
-
console.error(' npm install -g @polderlabs/bizar --force');
|
|
333
|
-
} else {
|
|
334
|
-
console.log(' ✓ Dashboard source present (bizar-dash/)');
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
306
|
async function promptGraphifyInstall() {
|
|
339
307
|
const { spawnSync, execSync } = await import('node:child_process');
|
|
340
308
|
|
|
@@ -511,10 +479,19 @@ async function promptGraphifyInstall() {
|
|
|
511
479
|
}
|
|
512
480
|
|
|
513
481
|
export async function runPostInstall() {
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
482
|
+
// v6.1.0 — Bizar is Cline-only. The legacy opencode-era
|
|
483
|
+
// `promptAndInstallOptional()` (plugin/dashboard presence probes)
|
|
484
|
+
// has been removed; `cli/provision.mjs:runProvision` covers the same
|
|
485
|
+
// surface via `runProvision({ mode: 'install' })`, which is the path
|
|
486
|
+
// used by `bizar install` and `bizar update`. This `runPostInstall`
|
|
487
|
+
// is now a thin Cline-only bootstrap that:
|
|
488
|
+
// - copies `config/cline.json` template on first install
|
|
489
|
+
// - runs `installCommandsBizar()` for the legacy `commands-bizar/` dir
|
|
490
|
+
// - installs agents into `~/.cline/agents/`
|
|
491
|
+
// - probes for headroom / semble / skills-cli
|
|
492
|
+
// - installs Headroom via pip or npm
|
|
493
|
+
// The old OpenCode-era probes (`Plugin source present`,
|
|
494
|
+
// `Dashboard source present`) moved to `cli/doctor.mjs` as live checks.
|
|
518
495
|
const { mkdirSync, copyFileSync, existsSync } = await import('node:fs');
|
|
519
496
|
const { execSync } = await import('node:child_process');
|
|
520
497
|
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* Verifies the installation is functional by checking:
|
|
7
7
|
* 1. Memory vault exists and is git-initialized
|
|
8
8
|
* 2. `bizar doctor` exits 0
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
9
|
+
* 3. Dashboard HTTP responds 200
|
|
10
|
+
* 4. WebSocket connects to dashboard
|
|
11
|
+
* 5. `bizar bg list` returns (even if empty)
|
|
12
|
+
* 6. lightrag-server installed
|
|
11
13
|
*
|
|
12
14
|
* Each check has a 30s timeout. Exits 0 if all pass, 1 otherwise.
|
|
13
15
|
*
|
|
@@ -27,6 +29,7 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
27
29
|
const HOME = homedir();
|
|
28
30
|
const DEFAULT_BIZAR_HOME = join(HOME, '.config', 'bizar');
|
|
29
31
|
const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
|
|
32
|
+
const DEFAULT_DASHBOARD_PORT = 4097;
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* @param {object} opts
|
|
@@ -37,6 +40,7 @@ const DEFAULT_MEMORY_VAULT = join(HOME, '.bizar_memory');
|
|
|
37
40
|
export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
38
41
|
const bh = bizarHome || DEFAULT_BIZAR_HOME;
|
|
39
42
|
const mv = process.env.BIZAR_MEMORY_VAULT || DEFAULT_MEMORY_VAULT;
|
|
43
|
+
const port = parseInt(process.env.BIZAR_DASHBOARD_PORT || String(DEFAULT_DASHBOARD_PORT), 10) || DEFAULT_DASHBOARD_PORT;
|
|
40
44
|
const checks = [];
|
|
41
45
|
|
|
42
46
|
function check(name, fn) {
|
|
@@ -77,7 +81,96 @@ export async function runSmokeTest({ bizarHome, repoPath, timeoutMs = DEFAULT_TI
|
|
|
77
81
|
}
|
|
78
82
|
});
|
|
79
83
|
|
|
80
|
-
// ── 3.
|
|
84
|
+
// ── 3. Dashboard HTTP responds 200 (with retry) ─────────────────────────────────
|
|
85
|
+
check('dashboard HTTP', async () => {
|
|
86
|
+
const maxAttempts = 5;
|
|
87
|
+
const delayMs = 2000;
|
|
88
|
+
let lastErr = null;
|
|
89
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
90
|
+
try {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`http://127.0.0.1:${port}/`, {
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
method: 'GET',
|
|
97
|
+
});
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
if (attempt > 1) {
|
|
101
|
+
return { ok: true, message: `HTTP ${res.status} on port ${port} (tried ${attempt}x)` };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, message: `HTTP ${res.status} on port ${port}` };
|
|
104
|
+
}
|
|
105
|
+
lastErr = `HTTP ${res.status} on port ${port} (expected 200)`;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
lastErr = err.name === 'AbortError'
|
|
109
|
+
? `dashboard not responding on port ${port} (timeout)`
|
|
110
|
+
: `dashboard unreachable on port ${port}: ${err.message}`;
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
lastErr = `fetch not available: ${err.message}`;
|
|
114
|
+
}
|
|
115
|
+
if (attempt < maxAttempts) {
|
|
116
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { ok: false, message: lastErr || 'dashboard HTTP failed' };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── 4. WebSocket connects (with retry) ────────────────────────────────────────
|
|
123
|
+
check('dashboard WebSocket', async () => {
|
|
124
|
+
const maxAttempts = 5;
|
|
125
|
+
const delayMs = 2000;
|
|
126
|
+
let lastErr = null;
|
|
127
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
const { WebSocket } = await import('ws' in globalThis
|
|
130
|
+
? { ws: globalThis.ws, WebSocket: globalThis.WebSocket }
|
|
131
|
+
: await import(`ws`).then(m => ({ ws: m, WebSocket: m.WebSocket || m.default }))
|
|
132
|
+
);
|
|
133
|
+
const url = `ws://127.0.0.1:${port}/ws`;
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
136
|
+
try {
|
|
137
|
+
const ws = new WebSocket(url);
|
|
138
|
+
await new Promise((resolve, reject) => {
|
|
139
|
+
ws.on('open', resolve);
|
|
140
|
+
ws.on('error', reject);
|
|
141
|
+
ws.on('close', () => reject(new Error('closed before open')));
|
|
142
|
+
controller.signal.addEventListener('abort', () => {
|
|
143
|
+
ws.close();
|
|
144
|
+
reject(new Error('timeout'));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
ws.close();
|
|
149
|
+
if (attempt > 1) {
|
|
150
|
+
return { ok: true, message: `WebSocket connected at ${url} (tried ${attempt}x)` };
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, message: `WebSocket connected at ${url}` };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
lastErr = err.message === 'timeout'
|
|
156
|
+
? `WebSocket timeout on ${url}`
|
|
157
|
+
: `WebSocket failed on ${url}: ${err.message}`;
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// ws module not available in this environment — skip
|
|
161
|
+
if (err.code === 'MODULE_NOT_FOUND' || err.message?.includes('ws')) {
|
|
162
|
+
return { ok: true, message: 'ws module not available — skipping WebSocket check' };
|
|
163
|
+
}
|
|
164
|
+
lastErr = `WebSocket check failed: ${err.message}`;
|
|
165
|
+
}
|
|
166
|
+
if (attempt < maxAttempts) {
|
|
167
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { ok: false, message: lastErr || 'WebSocket failed' };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── 5. `bizar bg list` returns ────────────────────────────────────────────
|
|
81
174
|
check('background agents', () => {
|
|
82
175
|
try {
|
|
83
176
|
execSync('bizar bg list', {
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Tests:
|
|
7
7
|
* 1. lightrag-server check: file existence is verified
|
|
8
|
-
* 2.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (the dashboard is optional and the checks were failing noisily on
|
|
12
|
-
* systems that don't run it). See PROGRESS.md.
|
|
8
|
+
* 2. HTTP retry: retries on failure, succeeds when server eventually responds
|
|
9
|
+
* 3. WS retry: retries on failure, succeeds when server eventually responds
|
|
10
|
+
* 4. lightrag: warns when not found in PATH or ~/.local/bin/
|
|
13
11
|
*/
|
|
14
12
|
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
15
13
|
import assert from 'node:assert/strict';
|
package/cli/provision.mjs
CHANGED
|
@@ -860,8 +860,46 @@ export async function patchClineJson({ dryRun, force }) {
|
|
|
860
860
|
(p) => Array.isArray(p) && typeof p[0] === 'string' && p[0].includes('plugins/bizar'),
|
|
861
861
|
);
|
|
862
862
|
|
|
863
|
-
//
|
|
864
|
-
//
|
|
863
|
+
// v6.2.0 — Multi-patch installer. On every install/update, ensure the
|
|
864
|
+
// full Bizar config surface is present in cline.json:
|
|
865
|
+
//
|
|
866
|
+
// 1. provider.9router (preferred gateway since v6.0.1) — add if
|
|
867
|
+
// missing so the install is "complete" out of the box.
|
|
868
|
+
// 2. provider.minimax (legacy fallback) — kept for back-compat.
|
|
869
|
+
// 3. default_agent — set to "odin" if missing.
|
|
870
|
+
// 4. $schema — add if missing (helps editors validate).
|
|
871
|
+
// 5. instructions — point at .cline/instructions/bizar-tools.md if
|
|
872
|
+
// missing, so Cline loads the tool reference on every session.
|
|
873
|
+
// 6. plugin entry — add if missing (the critical one; without this
|
|
874
|
+
// the Bizar plugin never loads).
|
|
875
|
+
// 7. permissions — set to "allow" if missing.
|
|
876
|
+
// 8. snapshot — set to false if missing.
|
|
877
|
+
//
|
|
878
|
+
// All patches are additive and idempotent. We never overwrite a value
|
|
879
|
+
// the user has set; the `force` flag bypasses that protection for the
|
|
880
|
+
// plugin entry only.
|
|
881
|
+
let addedProvider = false;
|
|
882
|
+
let addedDefaultAgent = false;
|
|
883
|
+
let addedSchema = false;
|
|
884
|
+
let addedInstructions = false;
|
|
885
|
+
let addedPermissions = false;
|
|
886
|
+
let addedSnapshot = false;
|
|
887
|
+
let added9router = false;
|
|
888
|
+
|
|
889
|
+
if (!cfg.provider) cfg.provider = {};
|
|
890
|
+
|
|
891
|
+
// 1. provider.9router — preferred gateway.
|
|
892
|
+
if (!cfg.provider['9router'] && existsSync(join(REPO_ROOT, 'config', 'cline.json'))) {
|
|
893
|
+
try {
|
|
894
|
+
const tpl = JSON.parse(readFileSync(join(REPO_ROOT, 'config', 'cline.json'), 'utf8'));
|
|
895
|
+
if (tpl.provider && tpl.provider['9router']) {
|
|
896
|
+
cfg.provider['9router'] = tpl.provider['9router'];
|
|
897
|
+
added9router = true;
|
|
898
|
+
}
|
|
899
|
+
} catch { /* ignore template parse errors */ }
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// 2. provider.minimax — legacy fallback.
|
|
865
903
|
const DEFAULT_MINIMAX_BLOCK = {
|
|
866
904
|
options: {
|
|
867
905
|
baseURL: 'https://api.minimax.io/v1',
|
|
@@ -874,21 +912,59 @@ export async function patchClineJson({ dryRun, force }) {
|
|
|
874
912
|
'MiniMax-M3-Reasoning': { name: 'MiniMax M3 Reasoning', interleaved: { field: 'reasoning_details' }, reasoning: true },
|
|
875
913
|
},
|
|
876
914
|
};
|
|
877
|
-
let addedProvider = false;
|
|
878
|
-
if (!cfg.provider) {
|
|
879
|
-
cfg.provider = {};
|
|
880
|
-
}
|
|
881
915
|
if (!cfg.provider.minimax) {
|
|
882
916
|
cfg.provider.minimax = DEFAULT_MINIMAX_BLOCK;
|
|
883
917
|
addedProvider = true;
|
|
884
918
|
}
|
|
885
919
|
|
|
886
|
-
|
|
920
|
+
// 3. default_agent
|
|
921
|
+
if (!cfg.default_agent) {
|
|
922
|
+
cfg.default_agent = 'odin';
|
|
923
|
+
addedDefaultAgent = true;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 4. $schema
|
|
927
|
+
if (!cfg.$schema) {
|
|
928
|
+
cfg.$schema = 'https://docs.cline.bot/config.json';
|
|
929
|
+
addedSchema = true;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// 5. instructions — point at the bundled Bizar tools reference.
|
|
933
|
+
if (!cfg.instructions || (Array.isArray(cfg.instructions) && cfg.instructions.length === 0)) {
|
|
934
|
+
cfg.instructions = ['.cline/instructions/bizar-tools.md'];
|
|
935
|
+
addedInstructions = true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// 6. permission
|
|
939
|
+
if (!cfg.permission) {
|
|
940
|
+
cfg.permission = 'allow';
|
|
941
|
+
addedPermissions = true;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// 7. snapshot
|
|
945
|
+
if (typeof cfg.snapshot !== 'boolean') {
|
|
946
|
+
cfg.snapshot = false;
|
|
947
|
+
addedSnapshot = true;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const anyAdded = addedProvider || added9router || addedDefaultAgent || addedSchema
|
|
951
|
+
|| addedInstructions || addedPermissions || addedSnapshot;
|
|
952
|
+
|
|
953
|
+
if (hasEntry && !force && !anyAdded) {
|
|
887
954
|
return { ok: true, message: 'cline.json already has Bizar plugin entry' };
|
|
888
955
|
}
|
|
889
956
|
|
|
890
957
|
if (dryRun) {
|
|
891
|
-
|
|
958
|
+
const added = [];
|
|
959
|
+
if (!hasEntry) added.push('plugin entry');
|
|
960
|
+
if (added9router) added.push('provider.9router');
|
|
961
|
+
if (addedProvider) added.push('provider.minimax');
|
|
962
|
+
if (addedDefaultAgent) added.push('default_agent');
|
|
963
|
+
if (addedSchema) added.push('$schema');
|
|
964
|
+
if (addedInstructions) added.push('instructions');
|
|
965
|
+
if (addedPermissions) added.push('permission');
|
|
966
|
+
if (addedSnapshot) added.push('snapshot');
|
|
967
|
+
return { ok: true, message: `[dry-run] would patch cline.json with: ${added.join(', ')}` };
|
|
892
968
|
}
|
|
893
969
|
|
|
894
970
|
if (!hasEntry) {
|
|
@@ -904,16 +980,26 @@ export async function patchClineJson({ dryRun, force }) {
|
|
|
904
980
|
loopThresholdEscalate: 8,
|
|
905
981
|
loopThresholdBlock: 12,
|
|
906
982
|
loopWindowSize: 10,
|
|
983
|
+
clineruntimeMaxConsecutiveMistakes: 6,
|
|
907
984
|
}]);
|
|
908
985
|
cfg.plugin = plugins;
|
|
909
986
|
}
|
|
910
987
|
|
|
911
988
|
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
989
|
+
const added = [];
|
|
990
|
+
if (!hasEntry) added.push('plugin entry');
|
|
991
|
+
if (added9router) added.push('provider.9router');
|
|
992
|
+
if (addedProvider) added.push('provider.minimax');
|
|
993
|
+
if (addedDefaultAgent) added.push('default_agent');
|
|
994
|
+
if (addedSchema) added.push('$schema');
|
|
995
|
+
if (addedInstructions) added.push('instructions');
|
|
996
|
+
if (addedPermissions) added.push('permission');
|
|
997
|
+
if (addedSnapshot) added.push('snapshot');
|
|
912
998
|
return {
|
|
913
999
|
ok: true,
|
|
914
|
-
message:
|
|
915
|
-
?
|
|
916
|
-
: 'cline.json
|
|
1000
|
+
message: added.length > 0
|
|
1001
|
+
? `cline.json patched: ${added.join(', ')}`
|
|
1002
|
+
: 'cline.json already complete',
|
|
917
1003
|
};
|
|
918
1004
|
}
|
|
919
1005
|
|