@polderlabs/bizar 6.1.0 → 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/post-install-smoke.mjs +96 -3
- package/cli/post-install-smoke.test.mjs +3 -5
- package/cli/provision.mjs +97 -11
- 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/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
package/bizar-dash/DESIGN.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# DESIGN.md — Bizar Dashboard
|
|
2
2
|
|
|
3
|
-
> The BizarHarness dashboard.
|
|
4
|
-
> dense by intent. **v7.
|
|
5
|
-
>
|
|
6
|
-
>
|
|
3
|
+
> The BizarHarness dashboard. Norse-pantheon operator surface, dark-mode
|
|
4
|
+
> first, dense by intent. **v7.0** — adds the comprehensive view index,
|
|
5
|
+
> per-view data shapes, and the patterns needed for the eight secondary
|
|
6
|
+
> tabs (Overview, Agents, Active, Skills, Memory, Mods, Schedules,
|
|
7
|
+
> Settings) that v6 didn't fully document.
|
|
7
8
|
|
|
8
9
|
## 0 · How to use this file
|
|
9
10
|
|
|
@@ -18,12 +19,6 @@ Do not hardcode hex outside `:root`. Derive tints with
|
|
|
18
19
|
`color-mix(in oklch, var(--accent) 12%, transparent)` rather than
|
|
19
20
|
inventing new tokens.
|
|
20
21
|
|
|
21
|
-
**Reference priority.** When fresh product reference images are
|
|
22
|
-
attached for a redesign pass, they override older stylistic
|
|
23
|
-
assumptions in this document. Preserve the structural rules here, but
|
|
24
|
-
bind the live visual system to the reference shell, spacing, and state
|
|
25
|
-
language.
|
|
26
|
-
|
|
27
22
|
**Themes.** The dashboard ships two themes — **dark** (default) and
|
|
28
23
|
**light** (`[data-theme="light"]`). Both share the same token names;
|
|
29
24
|
only the values change. Every component must read correctly in both
|
|
@@ -50,7 +45,7 @@ them in order; if two conflict, the higher rule wins.
|
|
|
50
45
|
decoration.
|
|
51
46
|
2. **One accent, used with intent.** The accent earns its place by
|
|
52
47
|
signaling primary action, active state, or data emphasis. No AI
|
|
53
|
-
gradients. No
|
|
48
|
+
gradients. No purple washes behind text. No purple stripes on
|
|
54
49
|
container edges.
|
|
55
50
|
3. **State is signal, not decoration.** Active, streaming, awaiting,
|
|
56
51
|
error, success — every state must be distinguishable at a glance
|
|
@@ -69,8 +64,8 @@ them in order; if two conflict, the higher rule wins.
|
|
|
69
64
|
|
|
70
65
|
| | |
|
|
71
66
|
|--|--|
|
|
72
|
-
| **Wordmark** | Bizar
|
|
73
|
-
| **Version pill** | mono text on a
|
|
67
|
+
| **Wordmark** | Bizar · runic `ᛒ` glyph in `var(--accent)` |
|
|
68
|
+
| **Version pill** | mono text on a `var(--accent)` → `oklch(0.55 0.22 25)` gradient |
|
|
74
69
|
| **Voice** | Direct, technical, never marketing. "404 not found" not "Oops! Something went wrong." Numbers with units. |
|
|
75
70
|
| **Tab labels** | Single noun (`Chat`, `Agents`, `Tasks`). No icons-in-paragraphs. |
|
|
76
71
|
| **Status copy** | "Streaming" not "Loading…". "Awaiting your reply" not "Ready". "13 of 47" not "Lots". |
|
|
@@ -96,12 +91,12 @@ or external-tool interop.
|
|
|
96
91
|
### 3.1 Surface scale (dark, default)
|
|
97
92
|
|
|
98
93
|
```
|
|
99
|
-
--bg oklch(
|
|
100
|
-
--bg-elev oklch(
|
|
101
|
-
--bg-elev-2 oklch(
|
|
102
|
-
--bg-elev-3 oklch(
|
|
103
|
-
--border oklch(27% 0.
|
|
104
|
-
--border-strong oklch(
|
|
94
|
+
--bg oklch(15% 0.012 260) #0b0e14 page canvas
|
|
95
|
+
--bg-elev oklch(18% 0.012 260) #12161f cards, topbar, sidebars
|
|
96
|
+
--bg-elev-2 oklch(22% 0.014 260) #1a1f2b nested surfaces
|
|
97
|
+
--bg-elev-3 oklch(27% 0.016 260) #232a39 hover / pressed surfaces
|
|
98
|
+
--border oklch(27% 0.016 260) #232a39 hairlines
|
|
99
|
+
--border-strong oklch(33% 0.018 260) #2d3648 focus rings, dividers
|
|
105
100
|
```
|
|
106
101
|
|
|
107
102
|
`--bg-1` and `--bg-2` from v6.x are deprecated; use `--bg-elev` and
|
|
@@ -132,9 +127,9 @@ or external-tool interop.
|
|
|
132
127
|
|
|
133
128
|
| Token | Dark | Light | Use |
|
|
134
129
|
|---|---|---|---|
|
|
135
|
-
| `--accent` | `oklch(
|
|
136
|
-
| `--accent-2` | `oklch(
|
|
137
|
-
| `--accent-3` | `oklch(
|
|
130
|
+
| `--accent` | `oklch(0.62 0.18 273)` | `oklch(0.55 0.20 273)` | Primary action, active state, focus |
|
|
131
|
+
| `--accent-2` | `oklch(0.72 0.16 273)` | `oklch(0.48 0.20 273)` | Hover state on accent |
|
|
132
|
+
| `--accent-3` | `oklch(0.82 0.10 273)` | `oklch(0.40 0.18 273)` | Subdued accent text |
|
|
138
133
|
| `--accent-bg` | `color-mix(in oklch, var(--accent) 12%, transparent)` | same, 8% | Tinted panel backgrounds |
|
|
139
134
|
| `--accent-border` | `color-mix(in oklch, var(--accent) 40%, transparent)` | same, 30% | Tinted borders |
|
|
140
135
|
| `--accent-glow` | `color-mix(in oklch, var(--accent) 18%, transparent)` | same | Focus glow shadow |
|
|
@@ -1154,4 +1149,4 @@ component in `src/web/views/settings/`.
|
|
|
1154
1149
|
semantic spacing aliases, command palette.
|
|
1155
1150
|
- v2.x — Light theme, kanban board, hooks refactor.
|
|
1156
1151
|
|
|
1157
|
-
Future changes update this file first, then ship.
|
|
1152
|
+
Future changes update this file first, then ship.
|
package/bizar-dash/critique.json
CHANGED
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
"axes": {
|
|
5
5
|
"clarity": {
|
|
6
6
|
"score": 5,
|
|
7
|
-
"notes": "The
|
|
7
|
+
"notes": "The launcher now explains the package in a clear operator sequence, and the canvas surfaces the right command decisions without reading like a generic file list."
|
|
8
8
|
},
|
|
9
9
|
"hierarchy": {
|
|
10
10
|
"score": 5,
|
|
11
|
-
"notes": "
|
|
11
|
+
"notes": "The redesigned hero, signal strip, command panel, and dense row structures create stronger primary and secondary reading paths across the package entry pages."
|
|
12
12
|
},
|
|
13
13
|
"typography": {
|
|
14
|
-
"score":
|
|
15
|
-
"notes": "The
|
|
14
|
+
"score": 4,
|
|
15
|
+
"notes": "The scale is tighter and more technical, with clearer display versus mono support roles, while staying compact enough for operator tooling."
|
|
16
16
|
},
|
|
17
17
|
"motion": {
|
|
18
18
|
"score": 4,
|
|
19
|
-
"notes": "The package
|
|
19
|
+
"notes": "The package keeps restrained interaction behavior through hover, segmented controls, and persistent sidebar state without adding distracting motion."
|
|
20
20
|
},
|
|
21
21
|
"brand": {
|
|
22
22
|
"score": 5,
|
|
23
|
-
"notes": "The cooler cyan-forward palette,
|
|
23
|
+
"notes": "The cooler cyan-forward palette, denser shell treatment, single-accent discipline, and no-left-stripe state language now read as a more professional technical system."
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
package/cli/bin.mjs
CHANGED
|
@@ -128,6 +128,7 @@ function showHelp() {
|
|
|
128
128
|
workspace Manage workspaces (via the dashboard's HTTP API)
|
|
129
129
|
eval Evaluate AI agent outputs against golden fixtures
|
|
130
130
|
plan [v6.0.0+] Reserved for future plan management
|
|
131
|
+
validate Validate the Bizar install (21 checks)
|
|
131
132
|
|
|
132
133
|
Examples:
|
|
133
134
|
bizar install
|
|
@@ -203,7 +204,7 @@ async function main() {
|
|
|
203
204
|
const UTIL_COMMANDS = new Set([
|
|
204
205
|
'audit', 'init', 'export', 'test-gate', 'dev-link', 'dev-unlink',
|
|
205
206
|
'doctor', 'repair', 'heads-up', 'bg', 'digest', 'backup', 'restore',
|
|
206
|
-
'agent-browser', 'update', 'providers', 'plan',
|
|
207
|
+
'agent-browser', 'update', 'providers', 'plan', 'validate',
|
|
207
208
|
]);
|
|
208
209
|
const UTIL_ALIASES = new Set(['dashboard', 'agent-browser-up']);
|
|
209
210
|
let mod;
|
|
@@ -497,6 +498,23 @@ async function main() {
|
|
|
497
498
|
break;
|
|
498
499
|
}
|
|
499
500
|
|
|
501
|
+
case 'validate': {
|
|
502
|
+
const mod = await importCommand('validate');
|
|
503
|
+
if (!mod) {
|
|
504
|
+
console.error(chalk.red(` ✗ Could not load validate command module`));
|
|
505
|
+
process.exit(EXIT_ERROR);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
dbg('loaded command module:', 'validate');
|
|
509
|
+
const found = await mod.run(cmd, cmdArgs, isHelpRequest);
|
|
510
|
+
if (!found) {
|
|
511
|
+
console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
|
|
512
|
+
showHelp();
|
|
513
|
+
process.exit(EXIT_ERROR);
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
|
|
500
518
|
default: {
|
|
501
519
|
console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
|
|
502
520
|
showHelp();
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/validate.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.2.0 — `bizar validate` subcommand.
|
|
5
|
+
*
|
|
6
|
+
* The Cline-aware health check. Extends `bizar doctor` with a longer
|
|
7
|
+
* battery of checks specific to a "flawless Cline integration":
|
|
8
|
+
*
|
|
9
|
+
* - cline.json schema + plugin path
|
|
10
|
+
* - 14 agent files in ~/.cline/agents/
|
|
11
|
+
* - 13 skills mirrored from config/skills/ → ~/.cline/skills/
|
|
12
|
+
* - 7 rules mirrored from config/rules/ → ~/.cline/rules/
|
|
13
|
+
* - 3 hooks in ~/.cline/hooks/ (pre/post-tool-use + README)
|
|
14
|
+
* - 13 slash commands in ~/.cline/commands/ (incl. /team /test /validate)
|
|
15
|
+
* - provider config sanity (9router preferred, minimax fallback)
|
|
16
|
+
* - 9router reachability
|
|
17
|
+
* - plugin runtime deps wired (zod, @cline/sdk, @cline/core)
|
|
18
|
+
* - enableAgentTeams plumbing (via the plugin index)
|
|
19
|
+
*
|
|
20
|
+
* Exits non-zero if any check fails. Use `--json` for machine output.
|
|
21
|
+
* Use `--strict` to also fail on lenient checks (9router unreachable).
|
|
22
|
+
*/
|
|
23
|
+
import chalk from 'chalk';
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
|
|
29
|
+
const HOME = homedir();
|
|
30
|
+
|
|
31
|
+
function clineDir() {
|
|
32
|
+
if (process.env.CLINE_DIR && process.env.CLINE_DIR.trim()) {
|
|
33
|
+
return process.env.CLINE_DIR.trim();
|
|
34
|
+
}
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
return join(process.env.APPDATA || HOME, 'cline');
|
|
37
|
+
}
|
|
38
|
+
return join(HOME, '.cline');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REPO_ROOT = process.env.BIZAR_REPO_ROOT || process.cwd();
|
|
42
|
+
|
|
43
|
+
const REQUIRED_AGENTS = [
|
|
44
|
+
'odin.md', 'vor.md', 'frigg.md', 'quick.md',
|
|
45
|
+
'mimir.md', 'heimdall.md', 'hermod.md', 'thor.md', 'baldr.md',
|
|
46
|
+
'tyr.md', 'vidarr.md', 'forseti.md',
|
|
47
|
+
'semble-search.md', 'agent-browser.md',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const REQUIRED_COMMANDS = [
|
|
51
|
+
'audit.md', 'bizar.md', 'explain.md', 'init.md', 'learn.md',
|
|
52
|
+
'plan.md', 'plow-through.md', 'pr-review.md', 'tailscale-serve.md',
|
|
53
|
+
'visual-plan.md',
|
|
54
|
+
'team.md', 'test.md', 'validate.md',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const REQUIRED_HOOKS = [
|
|
58
|
+
'pre-tool-use.md', 'post-tool-use.md', 'README.md',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const REQUIRED_RUNTIME_DEPS = ['zod', '@cline/sdk', '@cline/core', '@cline/shared'];
|
|
62
|
+
|
|
63
|
+
function check(name, fn) {
|
|
64
|
+
return Promise.resolve()
|
|
65
|
+
.then(fn)
|
|
66
|
+
.then((message) => ({ name, ok: true, message: message || 'ok' }))
|
|
67
|
+
.catch((err) => ({ name, ok: false, message: (err && err.message) ? err.message : String(err) }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function clineJsonPath() {
|
|
71
|
+
return join(clineDir(), 'cline.json');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readJsonSafe(file) {
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(file)) return null;
|
|
77
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pluginDepsDir() {
|
|
84
|
+
return join(clineDir(), 'plugins', 'bizar', 'node_modules');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const CHECKS = {
|
|
88
|
+
'cline-cli-reachable': async () => {
|
|
89
|
+
const r = spawnSync('cline', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
90
|
+
if (r.status !== 0) throw new Error(`cline --version exited ${r.status}`);
|
|
91
|
+
return (r.stdout || r.stderr || '').trim().split('\n')[0] || 'cline available';
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
'cline-json-exists': async () => {
|
|
95
|
+
const p = clineJsonPath();
|
|
96
|
+
if (!existsSync(p)) throw new Error(`not found at ${p}`);
|
|
97
|
+
return p;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
'cline-json-parses': async () => {
|
|
101
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
102
|
+
if (!cfg) throw new Error('invalid JSON');
|
|
103
|
+
return 'cline.json parses';
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
'plugin-entry-present': async () => {
|
|
107
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
108
|
+
const plugins = Array.isArray(cfg?.plugin) ? cfg.plugin : [];
|
|
109
|
+
const hasBizar = plugins.some((p) => {
|
|
110
|
+
if (Array.isArray(p) && typeof p[0] === 'string') return p[0].includes('plugins/bizar');
|
|
111
|
+
if (typeof p === 'string') return p.includes('plugins/bizar');
|
|
112
|
+
if (p && typeof p === 'object') {
|
|
113
|
+
return (p.path || '').includes('plugins/bizar');
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
});
|
|
117
|
+
if (!hasBizar) throw new Error('bizar plugin entry not in cline.json');
|
|
118
|
+
return 'bizar plugin registered';
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
'plugin-path-resolves': async () => {
|
|
122
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
123
|
+
const plugins = Array.isArray(cfg?.plugin) ? cfg.plugin : [];
|
|
124
|
+
let lastChecked = null;
|
|
125
|
+
for (const p of plugins) {
|
|
126
|
+
let entryPath = null;
|
|
127
|
+
if (Array.isArray(p) && typeof p[0] === 'string') entryPath = p[0];
|
|
128
|
+
else if (p && typeof p === 'object' && p.path) entryPath = p.path;
|
|
129
|
+
if (!entryPath) continue;
|
|
130
|
+
const isAbs = entryPath.startsWith('/') || /^[a-z]:[\\/]/i.test(entryPath);
|
|
131
|
+
const resolved = isAbs ? entryPath : join(clineDir(), entryPath);
|
|
132
|
+
lastChecked = resolved;
|
|
133
|
+
if (!existsSync(resolved)) {
|
|
134
|
+
throw new Error(`plugin path does not exist: ${resolved}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return lastChecked ? `plugin path resolves: ${lastChecked}` : 'no plugin path';
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
'plugin-runtime-deps': async () => {
|
|
141
|
+
const nm = pluginDepsDir();
|
|
142
|
+
if (!existsSync(nm)) {
|
|
143
|
+
throw new Error(`${nm} missing — run \`bizar update\``);
|
|
144
|
+
}
|
|
145
|
+
const missing = REQUIRED_RUNTIME_DEPS.filter((d) => !existsSync(join(nm, d)));
|
|
146
|
+
if (missing.length > 0) {
|
|
147
|
+
throw new Error(`missing runtime deps: ${missing.join(', ')} — run \`bizar update\``);
|
|
148
|
+
}
|
|
149
|
+
return `runtime deps present (${REQUIRED_RUNTIME_DEPS.join(', ')})`;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
'agent-files-installed': async () => {
|
|
153
|
+
const dir = join(clineDir(), 'agents');
|
|
154
|
+
if (!existsSync(dir)) throw new Error(`agents dir missing: ${dir}`);
|
|
155
|
+
const missing = REQUIRED_AGENTS.filter((f) => !existsSync(join(dir, f)));
|
|
156
|
+
if (missing.length > 0) throw new Error(`missing: ${missing.join(', ')}`);
|
|
157
|
+
return `all ${REQUIRED_AGENTS.length} agents present`;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
'agent-yaml-format': async () => {
|
|
161
|
+
// Cline loads agents via .yaml files generated from .md frontmatter.
|
|
162
|
+
// Verify at least one .yaml exists alongside its .md.
|
|
163
|
+
const dir = join(clineDir(), 'agents');
|
|
164
|
+
if (!existsSync(dir)) throw new Error('agents dir missing');
|
|
165
|
+
const yamls = readdirSync(dir).filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
166
|
+
if (yamls.length === 0) {
|
|
167
|
+
throw new Error('no .yaml agent files — Cline may not load them');
|
|
168
|
+
}
|
|
169
|
+
return `${yamls.length} Cline-loadable .yaml agent file(s)`;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
'slash-commands-installed': async () => {
|
|
173
|
+
const dir = join(clineDir(), 'commands');
|
|
174
|
+
if (!existsSync(dir)) throw new Error(`commands dir missing: ${dir}`);
|
|
175
|
+
const missing = REQUIRED_COMMANDS.filter((f) => !existsSync(join(dir, f)));
|
|
176
|
+
if (missing.length > 0) {
|
|
177
|
+
throw new Error(`missing: ${missing.join(', ')} — run \`bizar update\``);
|
|
178
|
+
}
|
|
179
|
+
return `all ${REQUIRED_COMMANDS.length} slash commands present`;
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
'team-command-present': async () => {
|
|
183
|
+
// /team is the new Cline command. Make sure it's installed.
|
|
184
|
+
const team = join(clineDir(), 'commands', 'team.md');
|
|
185
|
+
if (!existsSync(team)) throw new Error(`${team} missing — run \`bizar update\``);
|
|
186
|
+
return '/team command installed';
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
'test-command-present': async () => {
|
|
190
|
+
const testCmd = join(clineDir(), 'commands', 'test.md');
|
|
191
|
+
if (!existsSync(testCmd)) throw new Error(`${testCmd} missing — run \`bizar update\``);
|
|
192
|
+
return '/test command installed';
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
'validate-command-present': async () => {
|
|
196
|
+
const v = join(clineDir(), 'commands', 'validate.md');
|
|
197
|
+
if (!existsSync(v)) throw new Error(`${v} missing — run \`bizar update\``);
|
|
198
|
+
return '/validate command installed';
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
'skills-installed': async () => {
|
|
202
|
+
const dir = join(clineDir(), 'skills');
|
|
203
|
+
if (!existsSync(dir)) throw new Error(`skills dir missing: ${dir} — run \`bizar update\``);
|
|
204
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
205
|
+
const skills = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
206
|
+
if (skills.length === 0) throw new Error('no skills installed — run `bizar update`');
|
|
207
|
+
return `${skills.length} skill(s) installed: ${skills.slice(0, 5).join(', ')}${skills.length > 5 ? '...' : ''}`;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
'rules-installed': async () => {
|
|
211
|
+
const dir = join(clineDir(), 'rules');
|
|
212
|
+
if (!existsSync(dir)) throw new Error(`rules dir missing: ${dir} — run \`bizar update\``);
|
|
213
|
+
const entries = readdirSync(dir).filter((f) => f.endsWith('.md') || f.endsWith('.txt'));
|
|
214
|
+
if (entries.length === 0) throw new Error('no rules installed — run `bizar update`');
|
|
215
|
+
return `${entries.length} rule file(s): ${entries.join(', ')}`;
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
'hooks-installed': async () => {
|
|
219
|
+
const dir = join(clineDir(), 'hooks');
|
|
220
|
+
if (!existsSync(dir)) throw new Error(`hooks dir missing: ${dir} — run \`bizar update\``);
|
|
221
|
+
const missing = REQUIRED_HOOKS.filter((f) => !existsSync(join(dir, f)));
|
|
222
|
+
if (missing.length > 0) {
|
|
223
|
+
throw new Error(`missing: ${missing.join(', ')} — run \`bizar update\``);
|
|
224
|
+
}
|
|
225
|
+
return `all ${REQUIRED_HOOKS.length} hook file(s) present`;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
'provider-config': async () => {
|
|
229
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
230
|
+
if (!cfg?.provider) throw new Error('no provider block in cline.json');
|
|
231
|
+
const nine = cfg.provider['9router'];
|
|
232
|
+
if (nine && nine.baseUrl) {
|
|
233
|
+
const models = nine.models || {};
|
|
234
|
+
const count = Object.keys(models).length;
|
|
235
|
+
if (count === 0) throw new Error('provider.9router has no models');
|
|
236
|
+
return `provider.9router (${count} models, baseUrl=${nine.baseUrl})`;
|
|
237
|
+
}
|
|
238
|
+
const minimax = cfg.provider.minimax;
|
|
239
|
+
if (!minimax) {
|
|
240
|
+
throw new Error('no provider.9router AND no provider.minimax');
|
|
241
|
+
}
|
|
242
|
+
return 'provider.minimax (legacy fallback)';
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
'9router-reachable': async () => {
|
|
246
|
+
const url = process.env.NINEROUTER_URL || 'http://localhost:20128';
|
|
247
|
+
const ac = new AbortController();
|
|
248
|
+
const timer = setTimeout(() => ac.abort(), 4000);
|
|
249
|
+
try {
|
|
250
|
+
const res = await fetch(`${url}/api/health`, { signal: ac.signal });
|
|
251
|
+
if (!res.ok) throw new Error(`9router at ${url} returned HTTP ${res.status}`);
|
|
252
|
+
const body = await res.text().catch(() => '');
|
|
253
|
+
if (!body.includes('"ok":true')) {
|
|
254
|
+
throw new Error(`9router at ${url} responded but body did not include ok:true`);
|
|
255
|
+
}
|
|
256
|
+
return `9router healthy at ${url}`;
|
|
257
|
+
} finally {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
'default-agent-set': async () => {
|
|
263
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
264
|
+
if (!cfg?.default_agent) {
|
|
265
|
+
throw new Error('no default_agent in cline.json — should be "odin"');
|
|
266
|
+
}
|
|
267
|
+
return `default_agent=${cfg.default_agent}`;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
'instructions-loaded': async () => {
|
|
271
|
+
const cfg = readJsonSafe(clineJsonPath());
|
|
272
|
+
if (!cfg?.instructions || (Array.isArray(cfg.instructions) && cfg.instructions.length === 0)) {
|
|
273
|
+
throw new Error('no instructions[] in cline.json — Cline won\'t load the tools reference');
|
|
274
|
+
}
|
|
275
|
+
const ins = Array.isArray(cfg.instructions) ? cfg.instructions : [cfg.instructions];
|
|
276
|
+
return `${ins.length} instruction file(s) referenced`;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
'plugin-index-loadable': async () => {
|
|
280
|
+
// Spot-check that the deployed plugin's index.ts is on disk and looks
|
|
281
|
+
// like a Cline plugin (has the `setup` function + registerTool pattern).
|
|
282
|
+
const idx = join(clineDir(), 'plugins', 'bizar', 'index.ts');
|
|
283
|
+
if (!existsSync(idx)) {
|
|
284
|
+
throw new Error(`${idx} missing — plugin not copied`);
|
|
285
|
+
}
|
|
286
|
+
const stat = statSync(idx);
|
|
287
|
+
if (stat.size < 100) {
|
|
288
|
+
throw new Error(`${idx} is suspiciously small (${stat.size} bytes)`);
|
|
289
|
+
}
|
|
290
|
+
return `plugin index.ts: ${stat.size} bytes`;
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
'enable-agent-teams': async () => {
|
|
294
|
+
// The /team command depends on `enableAgentTeams: true` in
|
|
295
|
+
// plugins/bizar/src/clineruntime.ts. Static check by reading the
|
|
296
|
+
// source file from the deployed plugin dir.
|
|
297
|
+
const rt = join(clineDir(), 'plugins', 'bizar', 'src', 'clineruntime.ts');
|
|
298
|
+
if (!existsSync(rt)) {
|
|
299
|
+
// Source-only install (npm bundled): can't check here, skip leniently.
|
|
300
|
+
return 'skipped (bundled install)';
|
|
301
|
+
}
|
|
302
|
+
const text = readFileSync(rt, 'utf8');
|
|
303
|
+
if (!/enableAgentTeams:\s*true/.test(text)) {
|
|
304
|
+
throw new Error('enableAgentTeams is not true in clineruntime.ts — /team will not work');
|
|
305
|
+
}
|
|
306
|
+
return 'enableAgentTeams: true in clineruntime.ts';
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const CHECK_ORDER = [
|
|
311
|
+
'cline-cli-reachable',
|
|
312
|
+
'cline-json-exists',
|
|
313
|
+
'cline-json-parses',
|
|
314
|
+
'plugin-entry-present',
|
|
315
|
+
'plugin-path-resolves',
|
|
316
|
+
'plugin-runtime-deps',
|
|
317
|
+
'plugin-index-loadable',
|
|
318
|
+
'enable-agent-teams',
|
|
319
|
+
'agent-files-installed',
|
|
320
|
+
'agent-yaml-format',
|
|
321
|
+
'slash-commands-installed',
|
|
322
|
+
'team-command-present',
|
|
323
|
+
'test-command-present',
|
|
324
|
+
'validate-command-present',
|
|
325
|
+
'skills-installed',
|
|
326
|
+
'rules-installed',
|
|
327
|
+
'hooks-installed',
|
|
328
|
+
'default-agent-set',
|
|
329
|
+
'instructions-loaded',
|
|
330
|
+
'provider-config',
|
|
331
|
+
'9router-reachable',
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
const LENIENT_CHECKS = new Set(['9router-reachable']);
|
|
335
|
+
|
|
336
|
+
export function showValidateHelp() {
|
|
337
|
+
console.log(`
|
|
338
|
+
bizar validate — Validate the Bizar install end-to-end.
|
|
339
|
+
|
|
340
|
+
Usage:
|
|
341
|
+
bizar validate Run the full check battery (default)
|
|
342
|
+
bizar validate --json Machine-readable JSON output
|
|
343
|
+
bizar validate --strict Fail on lenient checks (e.g. 9router offline)
|
|
344
|
+
bizar validate --only <name> Run only the named check (e.g. team-command-present)
|
|
345
|
+
bizar validate --help Show this help
|
|
346
|
+
|
|
347
|
+
Description:
|
|
348
|
+
A 21-point health check that confirms the Bizar install is fully
|
|
349
|
+
integrated with Cline:
|
|
350
|
+
• cline CLI reachable + version
|
|
351
|
+
• cline.json parses + plugin entry + path resolves
|
|
352
|
+
• plugin runtime deps (zod, @cline/sdk, @cline/core) wired
|
|
353
|
+
• plugin index.ts + enableAgentTeams plumbing
|
|
354
|
+
• all 14 agent files installed + Cline .yaml format
|
|
355
|
+
• all 13 slash commands (/audit, /bizar, /explain, /init, /learn,
|
|
356
|
+
/plan, /plow-through, /pr-review, /tailscale-serve,
|
|
357
|
+
/visual-plan, /team, /test, /validate)
|
|
358
|
+
• all skills / rules / hooks mirrored to ~/.cline/
|
|
359
|
+
• provider.9router (preferred) or provider.minimax (legacy)
|
|
360
|
+
• 9Router gateway reachable (lenient unless --strict)
|
|
361
|
+
• default_agent + instructions[] in cline.json
|
|
362
|
+
|
|
363
|
+
Exit codes:
|
|
364
|
+
0 All checks passed (or only lenient ones failed)
|
|
365
|
+
1 One or more strict checks failed
|
|
366
|
+
2 Invalid --only argument
|
|
367
|
+
|
|
368
|
+
Related:
|
|
369
|
+
bizar doctor Simpler 8-point check (legacy)
|
|
370
|
+
bizar update Refresh the install (re-runs syncConfigExtras)
|
|
371
|
+
bizar repair Fix common issues (stale symlinks, version drift)
|
|
372
|
+
`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function runValidate(opts = {}) {
|
|
376
|
+
const json = !!opts.json;
|
|
377
|
+
const strict = !!opts.strict;
|
|
378
|
+
const only = typeof opts.only === 'string' && opts.only.trim() ? opts.only.trim() : null;
|
|
379
|
+
const list = only ? [only] : CHECK_ORDER;
|
|
380
|
+
if (only && !CHECKS[only]) {
|
|
381
|
+
console.error(chalk.red(` ✗ Unknown check: ${only}`));
|
|
382
|
+
console.error(chalk.dim(` Run \`bizar validate\` to see all available checks.`));
|
|
383
|
+
process.exit(2);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const results = [];
|
|
388
|
+
for (const name of list) {
|
|
389
|
+
const fn = CHECKS[name];
|
|
390
|
+
if (!fn) continue;
|
|
391
|
+
const r = await check(name, fn);
|
|
392
|
+
results.push(r);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (json) {
|
|
396
|
+
process.stdout.write(JSON.stringify({ passed: results.filter((r) => r.ok).length, failed: results.filter((r) => !r.ok).length, results }, null, 2) + '\n');
|
|
397
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
398
|
+
const strictFailed = results.filter((r) => !r.ok && !LENIENT_CHECKS.has(r.name)).length;
|
|
399
|
+
process.exit(strict ? (failed > 0 ? 1 : 0) : (strictFailed > 0 ? 1 : 0));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const r of results) {
|
|
404
|
+
const isLenient = LENIENT_CHECKS.has(r.name);
|
|
405
|
+
const marker = r.ok ? chalk.green('✓') : (isLenient ? chalk.yellow('⚠') : chalk.red('✗'));
|
|
406
|
+
const label = r.name.padEnd(28);
|
|
407
|
+
const msg = r.ok ? chalk.dim(` ${r.message}`) : (isLenient ? chalk.yellow(` ${r.message}`) : chalk.red(` ${r.message}`));
|
|
408
|
+
console.log(` ${marker} ${label}${msg}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const passed = results.filter((r) => r.ok).length;
|
|
412
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
413
|
+
const strictFailed = results.filter((r) => !r.ok && !LENIENT_CHECKS.has(r.name)).length;
|
|
414
|
+
console.log('');
|
|
415
|
+
if (strictFailed === 0 && (failed === 0 || (!strict && failed === results.filter((r) => LENIENT_CHECKS.has(r.name)).length))) {
|
|
416
|
+
console.log(chalk.green(` ✓ ${passed} checks passed, ${failed} lenient warning(s)`));
|
|
417
|
+
process.exit(0);
|
|
418
|
+
} else {
|
|
419
|
+
console.log(chalk.yellow(` ⚠ ${passed} checks passed, ${failed} failed (${strictFailed} strict)`));
|
|
420
|
+
console.log(chalk.dim(` Run \`bizar update\` to refresh the install, or \`bizar repair\` for common fixes.`));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── run() entry point (used by bin.mjs dispatcher) ───────────────────────────
|
|
426
|
+
|
|
427
|
+
export async function run(name, args, isHelpRequest) {
|
|
428
|
+
if (name !== 'validate') return false;
|
|
429
|
+
if (isHelpRequest) {
|
|
430
|
+
showValidateHelp();
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
const json = args.includes('--json');
|
|
434
|
+
const strict = args.includes('--strict');
|
|
435
|
+
const onlyIdx = args.indexOf('--only');
|
|
436
|
+
const only = onlyIdx >= 0 ? args[onlyIdx + 1] : null;
|
|
437
|
+
await runValidate({ json, strict, only });
|
|
438
|
+
return true;
|
|
439
|
+
}
|