@polderlabs/bizar 10.12.0 → 10.12.1
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/.claude-plugin/plugin.json +1 -1
- package/cli/commands/install.mjs +10 -2
- package/cli/install/index.mjs +6 -5
- package/cli/provision.mjs +91 -16
- package/package.json +1 -1
- package/packages/sdk/dist/version.d.ts +1 -1
- package/packages/sdk/dist/version.js +1 -1
- package/packages/sdk/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "bizar-harness",
|
|
4
4
|
"displayName": "Bizar Harness",
|
|
5
|
-
"version": "10.12.
|
|
5
|
+
"version": "10.12.1",
|
|
6
6
|
"description": "Guarded multi-agent workflows for Claude Code with a single orchestrator, durable workflow state, and explicit human approval boundaries.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Polderlabs"
|
package/cli/commands/install.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import chalk from 'chalk';
|
|
|
8
8
|
import { runInstaller } from '../install.mjs';
|
|
9
9
|
import { runUpdate } from '../update.mjs';
|
|
10
10
|
import { runRepair } from '../repair.mjs';
|
|
11
|
+
import { parseFlags } from '../provision.mjs';
|
|
11
12
|
|
|
12
13
|
// ── Help texts ──────────────────────────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -18,7 +19,10 @@ export function showInstallHelp() {
|
|
|
18
19
|
Usage:
|
|
19
20
|
bizar install Install (or refresh) every component
|
|
20
21
|
bizar install --dry-run Print what would happen, change nothing
|
|
21
|
-
bizar install --force Overwrite existing files
|
|
22
|
+
bizar install --force Overwrite existing files AND prune stale
|
|
23
|
+
entries in ~/.claude/{agents,skills,
|
|
24
|
+
commands,rules,hooks}
|
|
25
|
+
bizar install --yes Assume yes for any non-destructive prompt
|
|
22
26
|
bizar install --help Show this help
|
|
23
27
|
|
|
24
28
|
Description:
|
|
@@ -89,7 +93,11 @@ export async function install(args, isHelpRequest) {
|
|
|
89
93
|
showInstallHelp();
|
|
90
94
|
return;
|
|
91
95
|
}
|
|
92
|
-
|
|
96
|
+
// parseFlags lives in cli/provision.mjs and is the canonical argv
|
|
97
|
+
// parser for the installer family. Reusing it keeps install and
|
|
98
|
+
// update in lockstep on flag semantics.
|
|
99
|
+
const { mode, dryRun, force, yes } = parseFlags(args);
|
|
100
|
+
await runInstaller({ mode, dryRun, force, yes });
|
|
93
101
|
// v4.4.3 — After install, repair any stale bin symlinks so the
|
|
94
102
|
// user picks up the new code.
|
|
95
103
|
try {
|
package/cli/install/index.mjs
CHANGED
|
@@ -13,12 +13,13 @@ import { printInstallLocations } from './paths.mjs';
|
|
|
13
13
|
* Thin orchestrator entry point.
|
|
14
14
|
* @param {object} opts
|
|
15
15
|
* @param {boolean} [opts.dryRun]
|
|
16
|
-
* @param {boolean} [opts.force]
|
|
17
|
-
* @param {boolean} [opts.quiet]
|
|
18
|
-
* @param {string} [opts.mode]
|
|
16
|
+
* @param {boolean} [opts.force] - overwrite existing files AND prune stale entries
|
|
17
|
+
* @param {boolean} [opts.quiet] - Only print the location card
|
|
18
|
+
* @param {string} [opts.mode] - 'install' | 'update'
|
|
19
|
+
* @param {boolean} [opts.yes] - assume yes for any non-destructive prompts
|
|
19
20
|
*/
|
|
20
21
|
export async function runInstaller(opts = {}) {
|
|
21
|
-
const { dryRun = false, force = false, quiet = false, mode = 'install' } = opts;
|
|
22
|
+
const { dryRun = false, force = false, quiet = false, mode = 'install', yes = false } = opts;
|
|
22
23
|
|
|
23
24
|
if (quiet) {
|
|
24
25
|
printInstallLocations({ dryRun, force });
|
|
@@ -28,5 +29,5 @@ export async function runInstaller(opts = {}) {
|
|
|
28
29
|
showBanner();
|
|
29
30
|
printInstallLocations({ dryRun, force });
|
|
30
31
|
|
|
31
|
-
return runProvision({ mode, dryRun, force });
|
|
32
|
+
return runProvision({ mode, dryRun, force, yes });
|
|
32
33
|
}
|
package/cli/provision.mjs
CHANGED
|
@@ -352,6 +352,69 @@ function syncDir(srcDir, destDir, opts = {}) {
|
|
|
352
352
|
return { copied, skipped };
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Remove entries in destDir that are not present in srcDir.
|
|
357
|
+
* Honors the same recursive shape and filter as syncDir().
|
|
358
|
+
* Used by --force installs to clear stale files left over from
|
|
359
|
+
* previous versions (e.g. agents renamed by F-112).
|
|
360
|
+
*
|
|
361
|
+
* Policy: `bizar install --force` treats the global state under
|
|
362
|
+
* `~/.claude/{agents,skills,commands,rules,hooks}` as fully Bizar-
|
|
363
|
+
* managed. Anything in dest that does not appear in src is removed.
|
|
364
|
+
* This is intentionally aggressive; users who keep hand-edited
|
|
365
|
+
* entries under those paths should run without `--force` to leave
|
|
366
|
+
* them alone. See PROGRESS.md F-141 for the trade-off.
|
|
367
|
+
*
|
|
368
|
+
* Returns { removed, kept } counts.
|
|
369
|
+
*/
|
|
370
|
+
export function pruneStale(srcDir, destDir, opts = {}) {
|
|
371
|
+
if (!existsSync(destDir)) return { removed: 0, kept: 0 };
|
|
372
|
+
if (!existsSync(srcDir)) return { removed: 0, kept: 0 };
|
|
373
|
+
let removed = 0, kept = 0;
|
|
374
|
+
for (const name of readdirSync(destDir)) {
|
|
375
|
+
if (name.startsWith('.')) continue;
|
|
376
|
+
const dp = join(destDir, name);
|
|
377
|
+
const sp = join(srcDir, name);
|
|
378
|
+
let dstStat;
|
|
379
|
+
try { dstStat = statSync(dp); } catch { continue; }
|
|
380
|
+
const srcExists = existsSync(sp);
|
|
381
|
+
if (dstStat.isDirectory()) {
|
|
382
|
+
if (!srcExists) {
|
|
383
|
+
rmSync(dp, { recursive: true, force: true });
|
|
384
|
+
removed++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const r = pruneStale(sp, dp, opts);
|
|
388
|
+
removed += r.removed;
|
|
389
|
+
kept += r.kept;
|
|
390
|
+
// Drop now-empty directories.
|
|
391
|
+
try {
|
|
392
|
+
if (readdirSync(dp).length === 0) rmSync(dp, { recursive: true, force: true });
|
|
393
|
+
} catch { /* ignore */ }
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (!srcExists) {
|
|
397
|
+
if (opts.filter && !opts.filter(name, dp)) { kept++; continue; }
|
|
398
|
+
rmSync(dp, { force: true });
|
|
399
|
+
removed++;
|
|
400
|
+
} else {
|
|
401
|
+
kept++;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { removed, kept };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Run the prune pass under the same conditions used by every sync*
|
|
409
|
+
* helper and return a `{ pruned, tail }` pair the caller appends
|
|
410
|
+
* to its result message. No-op when `force` is false.
|
|
411
|
+
*/
|
|
412
|
+
function pruneReport(srcDir, destDir, filter, force) {
|
|
413
|
+
if (!force) return { pruned: 0, tail: '' };
|
|
414
|
+
const pruned = pruneStale(srcDir, destDir, { filter }).removed;
|
|
415
|
+
return { pruned, tail: pruned ? `, pruned ${pruned} stale` : '' };
|
|
416
|
+
}
|
|
417
|
+
|
|
355
418
|
// F-107: syncAgentFiles removed. Agent definitions live at
|
|
356
419
|
// .claude/agents/ (Claude Code canonical) — they were never sourced
|
|
357
420
|
// from config/agents/ in Claude Code era, and the legacy source dir
|
|
@@ -366,10 +429,11 @@ export async function syncAgentFiles({ dryRun = false, force = false } = {}) {
|
|
|
366
429
|
const src = join(REPO_ROOT, '.claude', 'agents');
|
|
367
430
|
const dest = CLAUDE_AGENTS_DIR;
|
|
368
431
|
if (!existsSync(src)) return { ok: true, message: `no agents source at ${src}`, copied: 0, skipped: 0 };
|
|
369
|
-
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
432
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}${force ? ' (prune stale)' : ''}` };
|
|
370
433
|
ensureDir(dest);
|
|
371
434
|
const { copied, skipped } = syncDir(src, dest, { filter: n => n.endsWith('.md') });
|
|
372
|
-
|
|
435
|
+
const { pruned, tail } = pruneReport(src, dest, n => n.endsWith('.md'), force);
|
|
436
|
+
return { ok: true, message: `${copied} agent(s) synced (${skipped} kept)${tail}`, copied, skipped, pruned };
|
|
373
437
|
}
|
|
374
438
|
|
|
375
439
|
// F-113: syncModelRouter added. The model-router.json file lives in
|
|
@@ -399,11 +463,11 @@ export async function syncModelRouter({ dryRun = false, force = false } = {}) {
|
|
|
399
463
|
return { ok: true, message: `model-router.json → ${dest}`, path: dest };
|
|
400
464
|
}
|
|
401
465
|
|
|
402
|
-
export async function syncSkillFiles({ dryRun = false } = {}) {
|
|
466
|
+
export async function syncSkillFiles({ dryRun = false, force = false } = {}) {
|
|
403
467
|
const src = join(REPO_ROOT, 'config', 'skills');
|
|
404
468
|
const dest = CLAUDE_SKILLS_DIR;
|
|
405
469
|
if (!existsSync(src)) return { ok: true, message: `no skills source at ${src}`, copied: 0, skipped: 0 };
|
|
406
|
-
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
470
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}${force ? ' (prune stale)' : ''}` };
|
|
407
471
|
ensureDir(dest); copyDirContents(src, dest);
|
|
408
472
|
const sharedBaseline = join(REPO_ROOT, 'config', 'agents', '_shared', 'AGENT_BASELINE.md');
|
|
409
473
|
if (existsSync(sharedBaseline)) {
|
|
@@ -411,39 +475,50 @@ export async function syncSkillFiles({ dryRun = false } = {}) {
|
|
|
411
475
|
ensureDir(baselineDir);
|
|
412
476
|
copyFileSync(sharedBaseline, join(baselineDir, 'SKILL.md'));
|
|
413
477
|
}
|
|
478
|
+
// Skill packs are directories containing SKILL.md; a stray non-SKILL.md
|
|
479
|
+
// file in dest is almost certainly user-owned. Restrict the prune pass
|
|
480
|
+
// to .md files only.
|
|
481
|
+
const { pruned, tail } = pruneReport(src, dest, n => n.endsWith('.md'), force);
|
|
414
482
|
const count = readdirSync(dest).filter(n => { try { return statSync(join(dest, n)).isDirectory(); } catch { return false; } }).length;
|
|
415
|
-
return { ok: true, message: `${count} skill(s) synced`, copied: count, skipped: 0 };
|
|
483
|
+
return { ok: true, message: `${count} skill(s) synced${tail}`, copied: count, skipped: 0, pruned };
|
|
416
484
|
}
|
|
417
485
|
|
|
418
|
-
export async function syncCommandFiles({ dryRun = false } = {}) {
|
|
486
|
+
export async function syncCommandFiles({ dryRun = false, force = false } = {}) {
|
|
419
487
|
const candidates = [join(REPO_ROOT, '.claude', 'commands'), join(REPO_ROOT, 'config', 'commands')];
|
|
420
488
|
let src = null;
|
|
421
489
|
for (const c of candidates) if (existsSync(c)) { src = c; break; }
|
|
422
490
|
if (!src) return { ok: true, message: 'no commands source found', copied: 0, skipped: 0 };
|
|
423
491
|
const dest = CLAUDE_COMMANDS_DIR;
|
|
424
|
-
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
492
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}${force ? ' (prune stale)' : ''}` };
|
|
425
493
|
ensureDir(dest);
|
|
426
494
|
const { copied, skipped } = syncDir(src, dest, { filter: n => n.endsWith('.md') });
|
|
427
|
-
|
|
495
|
+
const { pruned, tail } = pruneReport(src, dest, n => n.endsWith('.md'), force);
|
|
496
|
+
return { ok: true, message: `${copied} command(s) synced (${skipped} kept)${tail}`, copied, skipped, pruned };
|
|
428
497
|
}
|
|
429
498
|
|
|
430
|
-
export async function syncRulesFiles({ dryRun = false } = {}) {
|
|
499
|
+
export async function syncRulesFiles({ dryRun = false, force = false } = {}) {
|
|
431
500
|
const src = join(REPO_ROOT, 'config', 'rules');
|
|
432
501
|
const dest = CLAUDE_RULES_DIR;
|
|
433
502
|
if (!existsSync(src)) return { ok: true, message: `no rules source at ${src}`, copied: 0, skipped: 0 };
|
|
434
|
-
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
503
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}${force ? ' (prune stale)' : ''}` };
|
|
435
504
|
ensureDir(dest);
|
|
436
|
-
const
|
|
437
|
-
|
|
505
|
+
const ruleFilter = n => n.endsWith('.md') || n.endsWith('.txt');
|
|
506
|
+
const { copied, skipped } = syncDir(src, dest, { filter: ruleFilter });
|
|
507
|
+
const { pruned, tail } = pruneReport(src, dest, ruleFilter, force);
|
|
508
|
+
return { ok: true, message: `${copied} rule(s) synced (${skipped} kept)${tail}`, copied, skipped, pruned };
|
|
438
509
|
}
|
|
439
510
|
|
|
440
|
-
export async function syncHookFiles({ dryRun = false } = {}) {
|
|
511
|
+
export async function syncHookFiles({ dryRun = false, force = false } = {}) {
|
|
441
512
|
const src = join(REPO_ROOT, '.claude', 'hooks');
|
|
442
513
|
const dest = CLAUDE_HOOKS_DIR;
|
|
443
514
|
ensureDir(dest);
|
|
444
515
|
if (!existsSync(src)) return { ok: true, message: `no hooks source at ${src}`, copied: 0, skipped: 0 };
|
|
445
|
-
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}` };
|
|
516
|
+
if (dryRun) return { ok: true, message: `[dry-run] would sync ${src} → ${dest}${force ? ' (prune stale)' : ''}` };
|
|
446
517
|
copyDirContents(src, dest);
|
|
518
|
+
// Hooks are .mjs / .sh scripts. Restrict prune to those extensions so
|
|
519
|
+
// any user-owned file (READMEs, fixtures, etc.) is not removed.
|
|
520
|
+
const hookFilter = n => n.endsWith('.mjs') || n.endsWith('.sh');
|
|
521
|
+
const { pruned, tail } = pruneReport(src, dest, hookFilter, force);
|
|
447
522
|
for (const name of readdirSync(dest)) {
|
|
448
523
|
const fp = join(dest, name);
|
|
449
524
|
try {
|
|
@@ -451,7 +526,7 @@ export async function syncHookFiles({ dryRun = false } = {}) {
|
|
|
451
526
|
if (st.isFile() && (name.endsWith('.sh') || name.endsWith('.mjs'))) chmodSync(fp, 0o755);
|
|
452
527
|
} catch { /* ignore */ }
|
|
453
528
|
}
|
|
454
|
-
return { ok: true, message:
|
|
529
|
+
return { ok: true, message: `hook scripts installed${tail}`, copied: readdirSync(dest).length, skipped: 0, pruned };
|
|
455
530
|
}
|
|
456
531
|
|
|
457
532
|
// ─── git hooks ──────────────────────────────────────────────────────────────
|
|
@@ -992,7 +1067,7 @@ export async function syncConfigExtras({ dryRun = false } = {}) {
|
|
|
992
1067
|
|
|
993
1068
|
// ─── CLI entry ──────────────────────────────────────────────────────────────
|
|
994
1069
|
|
|
995
|
-
function parseFlags(argv) {
|
|
1070
|
+
export function parseFlags(argv) {
|
|
996
1071
|
const opts = { mode: 'install', dryRun: false, force: false, yes: false, start: true, update: false };
|
|
997
1072
|
for (let i = 0; i < argv.length; i++) {
|
|
998
1073
|
const a = argv[i];
|
package/package.json
CHANGED