@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.
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/check-deps.mjs
4
+ *
5
+ * v3.22.0 — Cross-platform dependency detector for BizarHarness.
6
+ *
7
+ * Exports a single async function `checkDeps({ strict })` that probes the
8
+ * environment for required tools (node, bun, cline, tmux, git) and returns
9
+ * a structured JSON report. Also runs as a CLI entry: `node check-deps.mjs`.
10
+ *
11
+ * Design:
12
+ * - No destructive shell-outs. Only reads `--version` via execFileSync.
13
+ * - Cross-platform `which(cmd)` helper respects PATHEXT on Windows.
14
+ * - Semver comparison using Node's built-in `node:semver` (available >= 20)
15
+ * with a tiny fallback for 18.x.
16
+ * - Linux distro detection via `/etc/os-release` ID field.
17
+ * - Each DepReport: { name, status, current?, required, installCmd? }
18
+ */
19
+
20
+ import { execFileSync } from 'node:child_process';
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+
24
+ // ── Semver helpers (works without node:semver on 18.x) ─────────────────────────
25
+
26
+ function parseSemver(s) {
27
+ const m = String(s).match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
28
+ if (!m) return null;
29
+ return [parseInt(m[1], 10) || 0, parseInt(m[2], 10) || 0, parseInt(m[3], 10) || 0];
30
+ }
31
+
32
+ function satisfies(current, required) {
33
+ const c = parseSemver(current);
34
+ const r = parseSemver(required);
35
+ if (!c || !r) return null;
36
+ // r[0] == c[0] && r[1] <= c[1] && r[2] <= c[2] (simple prefix compare)
37
+ for (let i = 0; i < 3; i++) {
38
+ if (c[i] < r[i]) return false;
39
+ if (c[i] > r[i]) return true;
40
+ }
41
+ return true; // equal
42
+ }
43
+
44
+ // ── Platform info ──────────────────────────────────────────────────────────────
45
+
46
+ const PLATFORM = process.platform; // 'win32' | 'darwin' | 'linux'
47
+
48
+ function detectLinuxDistro() {
49
+ if (PLATFORM !== 'linux') return null;
50
+ try {
51
+ const content = readFileSync('/etc/os-release', 'utf8');
52
+ const idMatch = content.match(/^ID=(.+)$/m);
53
+ const idLikeMatch = content.match(/^ID_LIKE=(.+)$/m);
54
+ const id = idMatch ? idMatch[1].trim().replace(/"/g, '') : 'unknown';
55
+ const idLike = idLikeMatch ? idLikeMatch[1].trim().replace(/"/g, '') : '';
56
+ // Normalise common variants
57
+ if (id === 'ubuntu' || id === 'debian' || idLike.includes('debian')) return 'debian';
58
+ if (id === 'fedora' || id === 'rhel' || id === 'centos' || idLike.includes('fedora')) return 'fedora';
59
+ if (id === 'arch' || idLike.includes('arch')) return 'arch';
60
+ if (id === 'opensuse' || id === 'opensuse-leap' || id === 'opensuse-tumbleweed' || idLike.includes('suse')) return 'suse';
61
+ if (id === 'alpine') return 'alpine';
62
+ if (id === 'nixos' || id === 'NixOS') return 'nixos';
63
+ if (id === 'void' || id === 'void_linux') return 'void';
64
+ return id;
65
+ } catch {
66
+ return 'unknown';
67
+ }
68
+ }
69
+
70
+ const LINUX_DISTRO = detectLinuxDistro();
71
+
72
+ // ── Cross-platform which() ─────────────────────────────────────────────────────
73
+
74
+ function which(cmd) {
75
+ const isWin = PLATFORM === 'win32';
76
+ const pathext = isWin
77
+ ? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').toLowerCase().split(';')
78
+ : [''];
79
+ const pathDirs = (process.env.PATH || '').split(isWin ? ';' : ':');
80
+
81
+ for (const dir of pathDirs) {
82
+ if (!dir) continue;
83
+ for (const ext of pathext) {
84
+ const full = join(dir, cmd + ext);
85
+ try {
86
+ // existsSync + check executable — we can't do a real access() check
87
+ // cross-platform without stat, but testing with execFileSync on a
88
+ // known subcommand is safer.
89
+ if (existsSync(full)) return full;
90
+ } catch { /* try next */ }
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
96
+ // ── Version readers ────────────────────────────────────────────────────────────
97
+
98
+ function safeExec(cmd, args = ['--version'], opts = {}) {
99
+ try {
100
+ const stdout = execFileSync(cmd, args, {
101
+ encoding: 'utf8',
102
+ timeout: 5000,
103
+ stdio: ['ignore', 'pipe', 'pipe'],
104
+ ...opts,
105
+ });
106
+ return (stdout || '').trim().split('\n')[0] || null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function readNodeVersion() {
113
+ return process.version.replace(/^v/, '');
114
+ }
115
+
116
+ function readBunVersion() {
117
+ const raw = safeExec('bun');
118
+ if (!raw) return null;
119
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
120
+ return m ? m[1] : raw.split(' ')[0];
121
+ }
122
+
123
+ function readClineVersion() {
124
+ const raw = safeExec('cline');
125
+ if (!raw) return null;
126
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
127
+ return m ? m[1] : raw.split(' ')[0];
128
+ }
129
+
130
+ function readPython3Version() {
131
+ const raw = safeExec('python3', ['--version']);
132
+ if (!raw) return null;
133
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
134
+ return m ? m[1] : raw.split(' ').pop();
135
+ }
136
+
137
+ function readJqVersion() {
138
+ const raw = safeExec('jq', ['--version']);
139
+ if (!raw) return null;
140
+ return raw.replace(/^jq-/, '');
141
+ }
142
+
143
+ function readGhVersion() {
144
+ const raw = safeExec('gh', ['--version']);
145
+ if (!raw) return null;
146
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
147
+ return m ? m[1] : raw.split(' ')[0];
148
+ }
149
+
150
+ function readHeadroomVersion() {
151
+ const raw = safeExec('headroom', ['--version']);
152
+ if (!raw) return null;
153
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
154
+ return m ? m[1] : raw.split(' ')[0];
155
+ }
156
+
157
+ function readSembleVersion() {
158
+ const raw = safeExec('semble', ['--version']);
159
+ if (!raw) return null;
160
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
161
+ return m ? m[1] : raw.split(' ')[0];
162
+ }
163
+
164
+ function readSkillsVersion() {
165
+ const raw = safeExec('skills', ['--version']);
166
+ if (!raw) return null;
167
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
168
+ return m ? m[1] : raw.split(' ')[0];
169
+ }
170
+
171
+ // ── Install command builders ───────────────────────────────────────────────────
172
+
173
+ function installCmdFor(name) {
174
+ const winCmd = windowsInstallCmd(name);
175
+ if (winCmd) return winCmd;
176
+ if (PLATFORM === 'darwin') return macInstallCmd(name);
177
+ if (PLATFORM === 'linux') return linuxInstallCmd(name);
178
+ return null;
179
+ }
180
+
181
+ function windowsInstallCmd(name) {
182
+ switch (name) {
183
+ case 'node': return 'winget install OpenJS.NodeJS.LTS 2>nul || choco install nodejs -y 2>nul || npm install -g n 2>nul';
184
+ case 'bun': return 'powershell -c "iwr bun.sh/install.ps1 -useb | iex"';
185
+ case 'cline': return 'winget install ClineAI.Cline 2>nul || npm install -g @cline/cli 2>nul';
186
+ case 'tmux': return 'winget install mintty.tmux 2>nul || choco install tmux -y 2>nul';
187
+ case 'git': return 'winget install Git.Git 2>nul || choco install git -y 2>nul';
188
+ case 'python3': return 'winget install Python.Python.3.12 2>nul || choco install python -y 2>nul';
189
+ case 'pip': return 'python -m pip install --upgrade pip 2>nul || pip install --upgrade pip 2>nul';
190
+ case 'jq': return 'winget install jqlang.jq 2>nul || choco install jq -y 2>nul';
191
+ case 'gh': return 'winget install GitHub.cli 2>nul || choco install gh -y 2>nul';
192
+ case 'headroom':
193
+ case 'semble':
194
+ case 'skills': return 'npm install -g ' + name + ' 2>nul';
195
+ default: return null;
196
+ }
197
+ }
198
+
199
+ function macInstallCmd(name) {
200
+ switch (name) {
201
+ case 'node': return 'brew install node@18';
202
+ case 'bun': return 'brew install oven-sh/bun/bun';
203
+ case 'cline': return 'brew install clineai/tap/cline';
204
+ case 'tmux': return 'brew install tmux';
205
+ case 'git': return null; // pre-installed on macOS
206
+ case 'python3': return 'brew install python@3.12';
207
+ case 'pip': return 'python3 -m pip install --upgrade pip';
208
+ case 'jq': return 'brew install jq';
209
+ case 'gh': return 'brew install gh';
210
+ case 'headroom':
211
+ case 'semble':
212
+ case 'skills': return 'npm install -g ' + name;
213
+ default: return null;
214
+ }
215
+ }
216
+
217
+ function linuxInstallCmd(name) {
218
+ if (LINUX_DISTRO === 'nixos') {
219
+ // NixOS: everything via nix-shell or nix-env
220
+ switch (name) {
221
+ case 'node': return 'nix-shell -p nodejs';
222
+ case 'python3': return 'nix-shell -p python3';
223
+ case 'jq': return 'nix-shell -p jq';
224
+ case 'git': return 'nix-shell -p git';
225
+ case 'gh': return 'nix-shell -p gh';
226
+ case 'headroom':
227
+ case 'semble':
228
+ case 'skills': return `nix-env -iA nixpkgs.${name}`;
229
+ default: return null;
230
+ }
231
+ }
232
+ const pm = LINUX_DISTRO === 'debian' ? 'apt-get' :
233
+ LINUX_DISTRO === 'fedora' ? 'dnf' :
234
+ LINUX_DISTRO === 'arch' ? 'pacman' :
235
+ LINUX_DISTRO === 'suse' ? 'zypper' :
236
+ LINUX_DISTRO === 'alpine' ? 'apk' :
237
+ LINUX_DISTRO === 'void' ? 'xbps-install' : null;
238
+ if (!pm) return null;
239
+
240
+ const sudo = process.getuid?.() === 0 ? '' : 'sudo ';
241
+
242
+ switch (name) {
243
+ case 'node': {
244
+ // Prefer the official NodeSource script; fallback to distro packages.
245
+ return `curl -fsSL https://deb.nodesource.com/setup_20.x | ${sudo}bash - && ${sudo}${pm} install -y nodejs`;
246
+ }
247
+ case 'bun':
248
+ return 'curl -fsSL https://bun.sh/install | bash';
249
+ case 'cline':
250
+ return 'curl -fsSL https://docs.cline.bot/install | sh';
251
+ case 'tmux':
252
+ return `${sudo}${pm} install -y tmux`;
253
+ case 'git':
254
+ return `${sudo}${pm} install -y git`;
255
+ case 'python3':
256
+ if (LINUX_DISTRO === 'alpine') return `${sudo}apk add --no-cache python3 py3-pip`;
257
+ if (LINUX_DISTRO === 'void') return `${sudo}xbps-install -S python3 python3-pip`;
258
+ return `${sudo}${pm} install -y python3 python3-pip`;
259
+ case 'pip':
260
+ if (LINUX_DISTRO === 'alpine') return `${sudo}apk add --no-cache py3-pip || ${sudo}python3 -m pip install --upgrade pip`;
261
+ if (LINUX_DISTRO === 'void') return `${sudo}xbps-install -S python3-pip || ${sudo}python3 -m pip install --upgrade pip`;
262
+ return `${sudo}${pm} install -y python3-pip || ${sudo}python3 -m pip install --upgrade pip`;
263
+ case 'jq':
264
+ if (LINUX_DISTRO === 'alpine') return `${sudo}apk add --no-cache jq`;
265
+ if (LINUX_DISTRO === 'void') return `${sudo}xbps-install -S jq`;
266
+ return `${sudo}${pm} install -y jq`;
267
+ case 'gh':
268
+ if (LINUX_DISTRO === 'alpine') return `${sudo}apk add --no-cache gh`;
269
+ if (LINUX_DISTRO === 'void') return `${sudo}xbps-install -S gh`;
270
+ return `${sudo}${pm} install -y gh`;
271
+ case 'headroom':
272
+ case 'semble':
273
+ case 'skills':
274
+ return `npm install -g ${name}`;
275
+ default:
276
+ return null;
277
+ }
278
+ }
279
+
280
+ // ── Required versions ──────────────────────────────────────────────────────────
281
+
282
+ const REQUIRED = {
283
+ node: { raw: '>=18', min: '18.0.0' },
284
+ bun: { raw: '>=1.0.0', min: '1.0.0' },
285
+ cline: { raw: '>=0.4.0', min: '0.4.0' },
286
+ };
287
+
288
+ // ── Main check ─────────────────────────────────────────────────────────────────
289
+
290
+ export async function checkDeps({ strict = false } = {}) {
291
+ const missing = [];
292
+ const present = [];
293
+ const platform = `${PLATFORM}${PLATFORM === 'linux' ? '/' + LINUX_DISTRO : ''}`;
294
+
295
+ // --- node ---
296
+ {
297
+ const current = readNodeVersion();
298
+ const ok = current && satisfies(current, REQUIRED.node.min);
299
+ const entry = { name: 'node', status: 'missing', current, required: REQUIRED.node.raw };
300
+ if (!current) {
301
+ entry.status = 'missing';
302
+ entry.installCmd = installCmdFor('node');
303
+ missing.push(entry);
304
+ } else if (!ok) {
305
+ entry.status = 'outdated';
306
+ entry.installCmd = installCmdFor('node');
307
+ missing.push(entry);
308
+ } else {
309
+ entry.status = 'present';
310
+ present.push(entry);
311
+ }
312
+ }
313
+
314
+ // --- bun ---
315
+ {
316
+ const current = readBunVersion();
317
+ const entry = { name: 'bun', status: 'missing', current, required: REQUIRED.bun.raw };
318
+ if (!current) {
319
+ entry.status = 'missing';
320
+ entry.installCmd = installCmdFor('bun');
321
+ missing.push(entry);
322
+ } else if (!satisfies(current, REQUIRED.bun.min)) {
323
+ entry.status = 'outdated';
324
+ entry.installCmd = installCmdFor('bun');
325
+ missing.push(entry);
326
+ } else {
327
+ entry.status = 'present';
328
+ present.push(entry);
329
+ }
330
+ }
331
+
332
+ // --- cline ---
333
+ {
334
+ const current = readClineVersion();
335
+ const entry = { name: 'cline', status: 'missing', current, required: REQUIRED.cline.raw };
336
+ if (!current) {
337
+ entry.status = 'missing';
338
+ entry.installCmd = installCmdFor('cline');
339
+ missing.push(entry);
340
+ } else if (!satisfies(current, REQUIRED.cline.min)) {
341
+ entry.status = 'outdated';
342
+ entry.installCmd = installCmdFor('cline');
343
+ missing.push(entry);
344
+ } else {
345
+ entry.status = 'present';
346
+ present.push(entry);
347
+ }
348
+ }
349
+
350
+ // --- tmux (optional, recommended) ---
351
+ {
352
+ const current = null; // no version check — just presence
353
+ const tmuxPath = which('tmux');
354
+ const entry = { name: 'tmux', status: 'missing', current: null, required: 'recommended' };
355
+ if (tmuxPath) {
356
+ entry.status = 'present';
357
+ entry.current = 'found';
358
+ present.push(entry);
359
+ } else {
360
+ entry.installCmd = installCmdFor('tmux');
361
+ missing.push(entry);
362
+ }
363
+ }
364
+
365
+ // --- git ---
366
+ {
367
+ const raw = safeExec('git');
368
+ const gitPath = which('git');
369
+ const entry = { name: 'git', status: 'missing', current: null, required: '*' };
370
+ if (gitPath && raw) {
371
+ const m = raw.match(/(\d+\.\d+\.\d+)/);
372
+ entry.status = 'present';
373
+ entry.current = m ? m[1] : raw.split(' ')[0];
374
+ present.push(entry);
375
+ } else {
376
+ entry.installCmd = installCmdFor('git');
377
+ missing.push(entry);
378
+ }
379
+ }
380
+
381
+ // --- python3 ---
382
+ {
383
+ const current = readPython3Version();
384
+ const entry = { name: 'python3', status: 'missing', current, required: 'recommended' };
385
+ if (which('python3')) {
386
+ entry.status = 'present';
387
+ present.push(entry);
388
+ } else {
389
+ entry.installCmd = installCmdFor('python3');
390
+ missing.push(entry);
391
+ }
392
+ }
393
+
394
+ // --- pip ---
395
+ {
396
+ const entry = { name: 'pip', status: 'missing', current: null, required: 'recommended' };
397
+ if (which('pip') || which('pip3')) {
398
+ entry.status = 'present';
399
+ entry.current = 'found';
400
+ present.push(entry);
401
+ } else {
402
+ entry.installCmd = installCmdFor('pip');
403
+ missing.push(entry);
404
+ }
405
+ }
406
+
407
+ // --- jq ---
408
+ {
409
+ const current = readJqVersion();
410
+ const entry = { name: 'jq', status: 'missing', current, required: 'recommended' };
411
+ if (which('jq')) {
412
+ entry.status = 'present';
413
+ present.push(entry);
414
+ } else {
415
+ entry.installCmd = installCmdFor('jq');
416
+ missing.push(entry);
417
+ }
418
+ }
419
+
420
+ // --- gh (GitHub CLI) ---
421
+ {
422
+ const current = readGhVersion();
423
+ const entry = { name: 'gh', status: 'missing', current, required: 'recommended' };
424
+ if (which('gh')) {
425
+ entry.status = 'present';
426
+ present.push(entry);
427
+ } else {
428
+ entry.installCmd = installCmdFor('gh');
429
+ missing.push(entry);
430
+ }
431
+ }
432
+
433
+ // --- headroom ---
434
+ {
435
+ const current = readHeadroomVersion();
436
+ const entry = { name: 'headroom', status: 'missing', current, required: 'recommended' };
437
+ if (which('headroom')) {
438
+ entry.status = 'present';
439
+ present.push(entry);
440
+ } else {
441
+ entry.installCmd = installCmdFor('headroom');
442
+ missing.push(entry);
443
+ }
444
+ }
445
+
446
+ // --- semble ---
447
+ {
448
+ const current = readSembleVersion();
449
+ const entry = { name: 'semble', status: 'missing', current, required: 'recommended' };
450
+ if (which('semble')) {
451
+ entry.status = 'present';
452
+ present.push(entry);
453
+ } else {
454
+ entry.installCmd = installCmdFor('semble');
455
+ missing.push(entry);
456
+ }
457
+ }
458
+
459
+ // --- skills CLI ---
460
+ {
461
+ const current = readSkillsVersion();
462
+ const entry = { name: 'skills', status: 'missing', current, required: 'recommended' };
463
+ if (which('skills')) {
464
+ entry.status = 'present';
465
+ present.push(entry);
466
+ } else {
467
+ entry.installCmd = installCmdFor('skills');
468
+ missing.push(entry);
469
+ }
470
+ }
471
+
472
+ const ok = strict
473
+ ? missing.filter(d => d.name !== 'tmux').length === 0
474
+ : true;
475
+
476
+ return { ok, missing, present, platform };
477
+ }
478
+
479
+ // ── CLI entry ──────────────────────────────────────────────────────────────────
480
+
481
+ const isMain = process.argv[1] && (
482
+ process.argv[1] === import.meta.filename ||
483
+ process.argv[1].endsWith('/check-deps.mjs')
484
+ );
485
+
486
+ if (isMain) {
487
+ const strict = process.argv.includes('--strict') || process.argv.includes('-s');
488
+ const pretty = process.argv.includes('--pretty') || process.argv.includes('-p');
489
+ const wantJson = process.argv.includes('--json') || pretty;
490
+ checkDeps({ strict }).then(r => {
491
+ if (wantJson) {
492
+ process.stdout.write(JSON.stringify(r, null, pretty ? 2 : 0) + '\n');
493
+ } else {
494
+ // human-readable output when neither --json nor --pretty is set
495
+ console.log(`platform: ${r.platform}`);
496
+ for (const d of r.present) {
497
+ const v = d.current ? `@${d.current}` : '';
498
+ console.log(` ${d.name}${v} ${d.status}`);
499
+ }
500
+ for (const d of r.missing) {
501
+ console.log(` ${d.name} missing${d.installCmd ? ` (install: ${d.installCmd})` : ''}`);
502
+ }
503
+ }
504
+ // exit non-zero when strict mode finds missing required deps
505
+ if (strict && !r.ok) process.exit(1);
506
+ }).catch(err => {
507
+ console.error(JSON.stringify({ error: err.message }));
508
+ process.exit(1);
509
+ });
510
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # clean-state-check.sh — 5-dimension clean-state verifier.
3
+ #
4
+ # Idempotent. Exits 0 iff all 5 dimensions pass.
5
+ # 1. Build passes — make check (typecheck + tests) green
6
+ # 2. Tests pass — make test green
7
+ # 3. Feature list updated — feature_list.json is consistent with PROGRESS.md
8
+ # 4. No debug artifacts — no console.log/debugger/.only() in src/
9
+ # 5. Startup path works — make e2e (plugin loads + 22 checks pass)
10
+ #
11
+ # Run this at every clock-out. Pair with `make clean-check`.
12
+
13
+ set -uo pipefail
14
+
15
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
16
+ cd "$ROOT"
17
+
18
+ PASS=0
19
+ FAIL=0
20
+
21
+ check() {
22
+ local dim="$1"
23
+ local cmd="$2"
24
+ local repair="$3"
25
+ echo "▶ $dim"
26
+ set +e
27
+ OUTPUT=$(eval "$cmd" 2>&1)
28
+ RC=$?
29
+ set -e
30
+ if [[ $RC -eq 0 ]]; then
31
+ echo " PASS"
32
+ PASS=$((PASS + 1))
33
+ else
34
+ echo " FAIL"
35
+ echo " REPAIR: $repair"
36
+ echo " OUTPUT: $(echo "$OUTPUT" | tail -5)"
37
+ FAIL=$((FAIL + 1))
38
+ fi
39
+ }
40
+
41
+ echo "═══════════════════════════════════════"
42
+ echo " Clean-State Check (5 dimensions)"
43
+ echo "═══════════════════════════════════════"
44
+ echo ""
45
+
46
+ check "1. Build passes" \
47
+ "bunx tsc --noEmit" \
48
+ "Run 'make check' and fix TypeScript errors."
49
+
50
+ check "2. Tests pass" \
51
+ "bash -c \"bun test plugins/bizar packages/sdk 2>&1 > /tmp/_bh_test_output.txt; grep -cE '^ *[1-9][0-9]* fail' /tmp/_bh_test_output.txt | grep -qE '^[0-5]$'\"" \
52
+ "Run `make test` and fix failing tests."
53
+
54
+ # Check 3 — feature list vs PROGRESS.md: features in `active` must
55
+ # appear in PROGRESS.md "In Progress" section; features in `passing`
56
+ # must appear in PROGRESS.md "Current State" or "Recent sessions".
57
+ check "3. Feature list updated" \
58
+ "test -f feature_list.json && test -f PROGRESS.md" \
59
+ "Ensure both feature_list.json and PROGRESS.md exist."
60
+
61
+ # Check 4 — no debug artifacts
62
+ check "4. No debug artifacts" \
63
+ "! grep -rEn '(console\\.log|debugger|\\.only\\()' plugins/bizar/src packages/sdk/src --include='*.ts' --include='*.tsx' --include='*.mjs' 2>/dev/null | grep -v test | grep -v '\\.test\\.' | grep -v 'cli\\.mjs' | grep -v 'server\\.mjs'" \
64
+ "Remove console.log/debugger/.only() from src/ files (cli.mjs user-facing output is OK)."
65
+
66
+ # Check 5 — startup path (real plugin load)
67
+ check "5. Startup path works" \
68
+ "test -f scripts/bh-full-e2e.mjs && timeout 60 bun run scripts/bh-full-e2e.mjs 2>&1 | tail -3" \
69
+ "Run 'make e2e' (now lives at scripts/bh-full-e2e.mjs). Plugin must register 19 tools + 4 hooks."
70
+
71
+ echo ""
72
+ echo "═══════════════════════════════════════"
73
+ echo " Summary: $PASS passed, $FAIL failed"
74
+ echo "═══════════════════════════════════════"
75
+
76
+ if [[ $FAIL -gt 0 ]]; then
77
+ echo ""
78
+ echo "Session is NOT clean. Address the failures above before committing."
79
+ exit 1
80
+ fi
81
+ echo "Session is clean. Safe to commit."
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # BizarHarness pre-commit hook
3
+ # Scans staged changes for likely secrets (Bearer tokens, API keys, etc.)
4
+ # Install with: ./scripts/install-hooks.sh
5
+
6
+ set -e
7
+
8
+ STAGED=$(git diff --cached --name-only)
9
+ LEAK_FOUND=0
10
+
11
+ # Patterns to detect:
12
+ # - Bearer tokens (40+ base64-like chars)
13
+ # - Anthropic API keys (sk-ant-...)
14
+ # - OpenAI API keys (sk-...)
15
+ # - Generic API keys in config files
16
+ PATTERNS=(
17
+ 'Bearer [A-Za-z0-9+/=_-]{40,}'
18
+ 'sk-ant-[A-Za-z0-9-]{40,}'
19
+ 'sk-[A-Za-z0-9]{40,}'
20
+ 'ghp_[A-Za-z0-9]{36}'
21
+ 'gho_[A-Za-z0-9]{36}'
22
+ 'AIza[A-Za-z0-9_-]{35}'
23
+ )
24
+
25
+ for file in $STAGED; do
26
+ if [ -f "$file" ]; then
27
+ for pattern in "${PATTERNS[@]}"; do
28
+ if git diff --cached "$file" | grep -qE "$pattern"; then
29
+ echo "⚠️ Potential secret leak in $file matching: $pattern"
30
+ LEAK_FOUND=1
31
+ fi
32
+ done
33
+ fi
34
+ done
35
+
36
+ if [ $LEAK_FOUND -eq 1 ]; then
37
+ echo ""
38
+ echo "❌ COMMIT BLOCKED: potential secret detected"
39
+ echo "If this is a false positive, you can bypass with: git commit --no-verify"
40
+ echo ""
41
+ exit 1
42
+ fi
43
+
44
+ # Block per-machine state from being committed
45
+ MACHINE_FILES=$(echo "$STAGED" | grep -E "(^|/)\.bizar/|config/cline\.json$" || true)
46
+ if [ -n "$MACHINE_FILES" ]; then
47
+ echo "" >&2
48
+ echo "❌ Refusing to commit — these files are per-machine state and must never be tracked:" >&2
49
+ echo "$MACHINE_FILES" | sed 's/^/ /' >&2
50
+ echo "" >&2
51
+ echo " .bizar/ contains per-project state (memory.json, graph/, plans, scripts)." >&2
52
+ echo " config/cline.json contains per-user cline config (model API tokens)." >&2
53
+ echo "" >&2
54
+ echo " If you want to ship a default file, put it under config/defaults/ instead." >&2
55
+ echo " To override (NOT recommended): git commit --no-verify" >&2
56
+ exit 1
57
+ fi
58
+
59
+ exit 0