@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.
@@ -1,9 +1,10 @@
1
1
  # DESIGN.md — Bizar Dashboard
2
2
 
3
- > The BizarHarness dashboard. Operator console surface, dark-mode first,
4
- > dense by intent. **v7.1** — image-led refresh toward flatter product
5
- > shells, restrained green signaling, and settings-grade control
6
- > surfaces.
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 decorative color washes behind text. No stripes on
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 / Agent Console · simple accent glyph in `var(--accent)` |
73
- | **Version pill** | mono text on a tinted neutral surface with accent border |
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(13.5% 0.003 220) page canvas
100
- --bg-elev oklch(16.5% 0.003 220) cards, topbar, sidebars
101
- --bg-elev-2 oklch(19.5% 0.004 220) nested surfaces
102
- --bg-elev-3 oklch(23% 0.005 220) hover / pressed surfaces
103
- --border oklch(27% 0.004 220) hairlines
104
- --border-strong oklch(34% 0.005 220) focus rings, dividers
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(74% 0.13 154)` | `oklch(56% 0.13 154)` | Primary action, active state, focus |
136
- | `--accent-2` | `oklch(67% 0.10 154)` | `oklch(50% 0.10 154)` | Hover state on accent |
137
- | `--accent-3` | `oklch(58% 0.07 154)` | `oklch(42% 0.08 154)` | Subdued accent text |
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.
@@ -4,23 +4,23 @@
4
4
  "axes": {
5
5
  "clarity": {
6
6
  "score": 5,
7
- "notes": "The package now explains both shell usage and data patterns more clearly, and the refined chart language makes dense panels easier to read quickly."
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": "Primary chart cards, legends, axes, queue rows, and new operator modules now carry clearer visual ranking without reverting to oversized UI."
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": 5,
15
- "notes": "The thinner graph strokes, mono summaries, and compact legends better match the technical type system and improve signal density."
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 still uses restrained interaction behavior, while the new chart system stays static and precise rather than decorative."
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, thin graph language, denser shell treatment, and expanded operator modules now read as a more complete technical system."
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
+ }