@polderlabs/bizar 10.16.0 → 10.16.2
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/cli/commands/install.mjs +34 -12
- package/cli/install/index.mjs +69 -4
- package/cli/provision.mjs +212 -6
- package/config/claude/hooks/agent-model-guard.mjs +5 -5
- package/config/claude/hooks/content-style-guard.mjs +2 -2
- package/config/claude/hooks/simplify-guard.mjs +3 -3
- package/package.json +1 -1
- package/scripts/git-hooks/pre-push +6 -0
package/cli/commands/install.mjs
CHANGED
|
@@ -19,9 +19,11 @@ export function showInstallHelp() {
|
|
|
19
19
|
Usage:
|
|
20
20
|
bizar install Install (or refresh) every component
|
|
21
21
|
bizar install --dry-run Print what would happen, change nothing
|
|
22
|
-
bizar install --force
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
bizar install --force Full clean install: wipe Bizar-managed dirs,
|
|
23
|
+
back up settings env vars, re-sync everything
|
|
24
|
+
from the repo. Preserves ~/.config/bizar/
|
|
25
|
+
login state. Combine with --yes to skip prompts.
|
|
26
|
+
bizar install --deep Alias for --force (clean-install semantics)
|
|
25
27
|
bizar install --yes Assume yes for any non-destructive prompt
|
|
26
28
|
bizar install --help Show this help
|
|
27
29
|
|
|
@@ -30,6 +32,21 @@ export function showInstallHelp() {
|
|
|
30
32
|
difference is just mode=install vs mode=update. Every step is
|
|
31
33
|
idempotent — running this twice is safe.
|
|
32
34
|
|
|
35
|
+
F-183 (v10.16.2+) — --force promotes the install from
|
|
36
|
+
overwrite+prune-stale to a fully clean install. It wipes:
|
|
37
|
+
- ~/.claude/{agents,skills,commands,hooks,rules,workflows,plugins}/
|
|
38
|
+
- ~/.agents/ (shared skill-registry + lock)
|
|
39
|
+
- ~/.claude/settings.json (env vars backed up to BIZAR_SAVED_ENV)
|
|
40
|
+
and preserves:
|
|
41
|
+
- ~/.config/bizar/ (login state, telemetry, model picks, worktree-queue)
|
|
42
|
+
- ~/.claude/.credentials.json, statsig/, .playwright-mcp/
|
|
43
|
+
- any user-created subdirs under ~/.claude/ outside the managed set
|
|
44
|
+
|
|
45
|
+
The freshly-emitted settings.json inherits the F-181 wildcard
|
|
46
|
+
permissions.allow expansion and the F-176 empty deny/ask lists
|
|
47
|
+
while the prior ANTHROPIC_* and BIZAR_* env vars are merged back
|
|
48
|
+
in from the stash.
|
|
49
|
+
|
|
33
50
|
1. Installs @polderlabs/bizar via npm (skipped if already current).
|
|
34
51
|
2. Shells to ./install.sh for platform-specific system dependencies.
|
|
35
52
|
3. Syncs agent files, slash commands, and bundled skills into
|
|
@@ -38,7 +55,6 @@ export function showInstallHelp() {
|
|
|
38
55
|
5. Wires Claude Code lifecycle hooks (SessionStart / PreToolUse /
|
|
39
56
|
PostToolUse / UserPromptSubmit) under ~/.claude/hooks/.
|
|
40
57
|
6. Runs 'bizar doctor' as a post-install health check.
|
|
41
|
-
|
|
42
58
|
No API key collection, no interactive prompts.
|
|
43
59
|
`);
|
|
44
60
|
}
|
|
@@ -48,7 +64,6 @@ export function showUpdateHelp() {
|
|
|
48
64
|
bizar update — Update @anthropic-ai/claude-code + @polderlabs/bizar
|
|
49
65
|
(which bundles the CLI, SDK, agents, skills, hooks, and commands). Detects what's
|
|
50
66
|
installed and only touches what's missing or out of date.
|
|
51
|
-
|
|
52
67
|
Usage:
|
|
53
68
|
bizar update Update all installed Bizar components
|
|
54
69
|
bizar update --check Only print current vs. latest; do not update
|
|
@@ -57,11 +72,9 @@ export function showUpdateHelp() {
|
|
|
57
72
|
bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
|
|
58
73
|
bizar update --yes Same as --force, but named for one-line scripts
|
|
59
74
|
bizar update --help Show this help
|
|
60
|
-
|
|
61
75
|
Components updated:
|
|
62
76
|
@anthropic-ai/claude-code the Claude Code CLI itself
|
|
63
77
|
@polderlabs/bizar CLI + SDK + agents + skills + hooks
|
|
64
|
-
|
|
65
78
|
Behavior (v4.4.7+):
|
|
66
79
|
• Single unified provisioner. 'bizar install' and 'bizar update' are
|
|
67
80
|
the same code path with different mode flags. Every step is
|
|
@@ -72,13 +85,11 @@ export function showUpdateHelp() {
|
|
|
72
85
|
regressions before claude tries to start.
|
|
73
86
|
• With --check: prints the version matrix and release-notes excerpt
|
|
74
87
|
between current and latest, exits non-zero if an update is available.
|
|
75
|
-
|
|
76
88
|
Examples:
|
|
77
89
|
bizar update Full auto-update (recommended)
|
|
78
90
|
bizar update --check Show version matrix + notes, do nothing
|
|
79
91
|
bizar update --channel=beta Upgrade to latest beta build
|
|
80
92
|
bizar update --dry-run Preview what would change
|
|
81
|
-
|
|
82
93
|
Errors:
|
|
83
94
|
Network failures (registry offline / DNS) and npm permission issues
|
|
84
95
|
are surfaced with the raw npm output. The provisioner never silently
|
|
@@ -87,7 +98,6 @@ export function showUpdateHelp() {
|
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
// ── Command runners ────────────────────────────────────────────────────────────
|
|
90
|
-
|
|
91
101
|
export async function install(args, isHelpRequest) {
|
|
92
102
|
if (isHelpRequest) {
|
|
93
103
|
showInstallHelp();
|
|
@@ -95,9 +105,21 @@ export async function install(args, isHelpRequest) {
|
|
|
95
105
|
}
|
|
96
106
|
// parseFlags lives in cli/provision.mjs and is the canonical argv
|
|
97
107
|
// parser for the installer family. Reusing it keeps install and
|
|
98
|
-
// update in lockstep on flag semantics.
|
|
108
|
+
// update in lockstep on flag semantics. --deep is parsed as an
|
|
109
|
+
// alias for --force (clean-install semantics, F-183).
|
|
99
110
|
const { mode, dryRun, force, yes } = parseFlags(args);
|
|
100
|
-
await runInstaller({ mode, dryRun, force, yes });
|
|
111
|
+
const result = await runInstaller({ mode, dryRun, force, yes });
|
|
112
|
+
// F-183 — print a one-line summary of the wipe scope so operators
|
|
113
|
+
// can see at a glance what changed without re-reading the verbose
|
|
114
|
+
// step list.
|
|
115
|
+
if (force && result?.clean) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(chalk.cyan(` Summary (F-183): wiped ${result.clean.wiped.length} path(s); preserved ${result.clean.preserved.length} path(s).`));
|
|
118
|
+
if (result.doctor) {
|
|
119
|
+
const ok = result.doctor.failed === 0;
|
|
120
|
+
console.log(chalk[ok ? 'green' : 'yellow'](` Doctor: ${result.doctor.passed} passed, ${result.doctor.failed} failed.`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
101
123
|
// v4.4.3 — After install, repair any stale bin symlinks so the
|
|
102
124
|
// user picks up the new code.
|
|
103
125
|
try {
|
package/cli/install/index.mjs
CHANGED
|
@@ -5,15 +5,29 @@
|
|
|
5
5
|
* that delegates to the provisioner.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { runProvision } from '../provision.mjs';
|
|
8
|
+
import { runProvision, forceCleanInstall, clearSavedEnv } from '../provision.mjs';
|
|
9
|
+
import { runDoctor } from '../doctor.mjs';
|
|
9
10
|
import { showBanner, sectionHeading } from './banner.mjs';
|
|
10
11
|
import { printInstallLocations } from './paths.mjs';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Thin orchestrator entry point.
|
|
15
|
+
*
|
|
16
|
+
* Behavior (v10.16.2+, F-183):
|
|
17
|
+
* - When `force === true`, run `forceCleanInstall` *before*
|
|
18
|
+
* `runProvision` so the Bizar-managed dirs under `~/.claude/` and
|
|
19
|
+
* `~/.agents/` are wiped, `~/.claude/settings.json` is removed, and
|
|
20
|
+
* the prior env vars are stashed into `process.env.BIZAR_SAVED_ENV`
|
|
21
|
+
* for the factory to re-inject. We always pass `force: true` to
|
|
22
|
+
* `runProvision` regardless of how the caller phrased the flag.
|
|
23
|
+
* - After the provisioner completes, run `runDoctor({ silent: true })`
|
|
24
|
+
* and surface the result so forced installs surface a health summary.
|
|
25
|
+
* - The wipe report is returned to the caller (e.g. `bizar install`)
|
|
26
|
+
* so the CLI layer can print a summary of what changed.
|
|
27
|
+
*
|
|
14
28
|
* @param {object} opts
|
|
15
29
|
* @param {boolean} [opts.dryRun]
|
|
16
|
-
* @param {boolean} [opts.force] - overwrite existing files AND prune stale entries
|
|
30
|
+
* @param {boolean} [opts.force] - overwrite existing files AND prune stale entries AND wipe dirs (F-183)
|
|
17
31
|
* @param {boolean} [opts.quiet] - Only print the location card
|
|
18
32
|
* @param {string} [opts.mode] - 'install' | 'update'
|
|
19
33
|
* @param {boolean} [opts.yes] - assume yes for any non-destructive prompts
|
|
@@ -29,5 +43,56 @@ export async function runInstaller(opts = {}) {
|
|
|
29
43
|
showBanner();
|
|
30
44
|
printInstallLocations({ dryRun, force });
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
// F-183 — pre-provision wipe for forced installs. Force-clean is what
|
|
47
|
+
// makes `--force` actually a clean install: dirs under ~/.claude/ are
|
|
48
|
+
// wiped (BIZAR_HOME and third-party state preserved), settings.json
|
|
49
|
+
// is removed, and the operator's env vars are stashed in
|
|
50
|
+
// `process.env.BIZAR_SAVED_ENV` so the next `writeClaudeSettings` call
|
|
51
|
+
// re-injects them after re-emitting the file from the template.
|
|
52
|
+
let clean = null;
|
|
53
|
+
if (force) {
|
|
54
|
+
clearSavedEnv();
|
|
55
|
+
clean = forceCleanInstall({ dryRun });
|
|
56
|
+
if (!quiet && clean?.wiped?.length) {
|
|
57
|
+
sectionHeading('Pre-install wipe (F-183)');
|
|
58
|
+
console.log(` wiped: ${clean.wiped.length} path(s)`);
|
|
59
|
+
for (const p of clean.wiped) console.log(` - ${p}`);
|
|
60
|
+
if (clean.preserved?.length) {
|
|
61
|
+
console.log(` preserved: ${clean.preserved.length} path(s)`);
|
|
62
|
+
for (const p of clean.preserved) console.log(` - ${p}`);
|
|
63
|
+
}
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Always pass `force: true` downstream so `runProvision` re-emits the
|
|
69
|
+
// template-owned keys (permissions.allow wildcards, mcpServers, hooks)
|
|
70
|
+
// into the freshly-empty settings file.
|
|
71
|
+
const provisionResult = await runProvision({ mode, dryRun, force: true, yes });
|
|
72
|
+
|
|
73
|
+
// F-183 — post-install health check. Surfaced as a warning rather
|
|
74
|
+
// than a hard failure so a forced install that completes without
|
|
75
|
+
// error still reports its doctor summary; the operator decides
|
|
76
|
+
// whether to investigate.
|
|
77
|
+
if (!dryRun) {
|
|
78
|
+
try {
|
|
79
|
+
const doctorResult = await runDoctor({ silent: true });
|
|
80
|
+
provisionResult.doctor = doctorResult;
|
|
81
|
+
if (!quiet) {
|
|
82
|
+
sectionHeading('Post-install doctor (F-183)');
|
|
83
|
+
const ok = doctorResult.failed === 0;
|
|
84
|
+
console.log(` ${ok ? '✓' : '✗'} doctor: ${doctorResult.passed} passed, ${doctorResult.failed} failed`);
|
|
85
|
+
if (doctorResult.failed > 0) {
|
|
86
|
+
for (const r of doctorResult.results) {
|
|
87
|
+
if (!r.ok) console.log(` ✗ ${r.name}: ${r.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (!quiet) console.log(` ! doctor skipped: ${err?.message || err}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ...provisionResult, clean };
|
|
98
|
+
}
|
package/cli/provision.mjs
CHANGED
|
@@ -740,7 +740,41 @@ export function writeClaudeSettings({ dryRun = false, force = false } = {}) {
|
|
|
740
740
|
const shipped = readJsonSafe(join(REPO_ROOT, 'config', 'claude', 'settings.json'), {}) || {};
|
|
741
741
|
const shippedRouter = readJsonSafe(join(REPO_ROOT, 'config', 'claude', 'model-router.json'), {}) || {};
|
|
742
742
|
const defaultGatewayUrl = shippedRouter.endpoint || shippedRouter.gateway?.endpoint || 'http://localhost:20129/v1';
|
|
743
|
-
|
|
743
|
+
|
|
744
|
+
// F-183 — when `forceCleanInstall` has wiped `~/.claude/settings.json`,
|
|
745
|
+
// it stashes the prior env block into `process.env.BIZAR_SAVED_ENV`.
|
|
746
|
+
// The stash is the **only** source of operator credentials that
|
|
747
|
+
// survives a wipe — everything else on disk has been removed by the
|
|
748
|
+
// time this function runs. We honor it preferentially for the env
|
|
749
|
+
// keys in FORCE_CLEAN_PRESERVE_ENV_KEYS so `bizar install --force`
|
|
750
|
+
// preserves gateway URLs / auth tokens without forcing operators to
|
|
751
|
+
// re-export them in the shell.
|
|
752
|
+
//
|
|
753
|
+
// Precedence (preserved-env keys):
|
|
754
|
+
// savedEnv.X > process.env.X > existingEnv.X > default
|
|
755
|
+
//
|
|
756
|
+
// The stash is only consulted when it was actually set by
|
|
757
|
+
// `forceCleanInstall` (non-empty JSON). For direct `force: true`
|
|
758
|
+
// calls that bypass the wipe, we keep the legacy precedence so the
|
|
759
|
+
// existing merge-settings contract is unchanged.
|
|
760
|
+
let savedEnv = {};
|
|
761
|
+
if (typeof process.env.BIZAR_SAVED_ENV === 'string') {
|
|
762
|
+
try { savedEnv = JSON.parse(process.env.BIZAR_SAVED_ENV) || {}; } catch { /* corrupt stash */ }
|
|
763
|
+
}
|
|
764
|
+
const hasSavedEnv = Object.keys(savedEnv).length > 0;
|
|
765
|
+
|
|
766
|
+
const pickEnv = (key) => {
|
|
767
|
+
if (hasSavedEnv && typeof savedEnv[key] === 'string') return savedEnv[key];
|
|
768
|
+
if (process.env[key]) return process.env[key];
|
|
769
|
+
if (!force && typeof existingEnv[key] === 'string') return existingEnv[key];
|
|
770
|
+
return undefined;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const savedBaseUrl = pickEnv('ANTHROPIC_BASE_URL');
|
|
774
|
+
const savedRouterUrl = pickEnv('BIZAR_MODEL_ROUTER_URL');
|
|
775
|
+
const gatewayUrl = savedBaseUrl
|
|
776
|
+
|| savedRouterUrl
|
|
777
|
+
|| process.env.ANTHROPIC_BASE_URL
|
|
744
778
|
|| process.env.BIZAR_MODEL_ROUTER_URL
|
|
745
779
|
|| (!force && (existingEnv.ANTHROPIC_BASE_URL || existingEnv.BIZAR_MODEL_ROUTER_URL))
|
|
746
780
|
|| defaultGatewayUrl;
|
|
@@ -808,12 +842,20 @@ export function writeClaudeSettings({ dryRun = false, force = false } = {}) {
|
|
|
808
842
|
env: {
|
|
809
843
|
BIZAR_HOME: BIZAR_HOME(),
|
|
810
844
|
ANTHROPIC_BASE_URL: gatewayUrl,
|
|
811
|
-
BIZAR_MODEL_ROUTER_URL:
|
|
812
|
-
|
|
845
|
+
BIZAR_MODEL_ROUTER_URL:
|
|
846
|
+
pickEnv('BIZAR_MODEL_ROUTER_URL')
|
|
847
|
+
|| gatewayUrl,
|
|
848
|
+
ANTHROPIC_AUTH_TOKEN:
|
|
849
|
+
pickEnv('ANTHROPIC_AUTH_TOKEN')
|
|
850
|
+
|| 'sk_9router',
|
|
813
851
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY:
|
|
814
|
-
|
|
852
|
+
pickEnv('CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY')
|
|
853
|
+
|| '1',
|
|
815
854
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS:
|
|
816
|
-
|
|
855
|
+
pickEnv('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS')
|
|
856
|
+
|| shipped.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
|
|
857
|
+
|| '1',
|
|
858
|
+
...(pickEnv('ANTHROPIC_MODEL') ? { ANTHROPIC_MODEL: pickEnv('ANTHROPIC_MODEL') } : {}),
|
|
817
859
|
},
|
|
818
860
|
hooks: {
|
|
819
861
|
UserPromptSubmit: [{ hooks: [hook('user-prompt-submit', 10)] }],
|
|
@@ -859,6 +901,12 @@ export function writeClaudeSettings({ dryRun = false, force = false } = {}) {
|
|
|
859
901
|
if (dryRun) return { ok: true, message: `[dry-run] would write ${fp}` };
|
|
860
902
|
ensureDir(CLAUDE_DIR);
|
|
861
903
|
writeFileSync(fp, JSON.stringify(merged, null, 2) + '\n');
|
|
904
|
+
// F-183 — clear the in-process stash once consumed so subsequent
|
|
905
|
+
// writes in the same run (or in tests) don't accidentally inherit
|
|
906
|
+
// operator credentials.
|
|
907
|
+
if (typeof process.env.BIZAR_SAVED_ENV === 'string') {
|
|
908
|
+
delete process.env.BIZAR_SAVED_ENV;
|
|
909
|
+
}
|
|
862
910
|
return { ok: true, message: `wrote ${fp}`, path: fp };
|
|
863
911
|
}
|
|
864
912
|
|
|
@@ -1164,6 +1212,164 @@ export async function syncConfigExtras({ dryRun = false } = {}) {
|
|
|
1164
1212
|
return { ok: true, message: `synced (${counts.commands} commands, ${counts.skills} skills, ${counts.hooks} hooks, ${counts.rules} rules, ${counts.workflows} workflows)`, counts };
|
|
1165
1213
|
}
|
|
1166
1214
|
|
|
1215
|
+
// ─── F-183 — fully clean install ────────────────────────────────────────────
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Env keys whose values the freshly-emitted `~/.claude/settings.json`
|
|
1219
|
+
* must inherit from a previously-installed copy of the file. Operators
|
|
1220
|
+
* run `bizar install --force` to repair drifted settings; this set is
|
|
1221
|
+
* the contract that keeps gateway credentials + provider URLs alive
|
|
1222
|
+
* across the wipe.
|
|
1223
|
+
*
|
|
1224
|
+
* Anything outside this set is **not** preserved — the re-emitted
|
|
1225
|
+
* `permissions` block must mirror the current template so the F-181
|
|
1226
|
+
* wildcard expansion and F-176 empty deny/ask lists land on disk.
|
|
1227
|
+
*/
|
|
1228
|
+
export const FORCE_CLEAN_PRESERVE_ENV_KEYS = Object.freeze([
|
|
1229
|
+
'ANTHROPIC_BASE_URL',
|
|
1230
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
1231
|
+
'BIZAR_MODEL_ROUTER_URL',
|
|
1232
|
+
'BIZAR_HOME',
|
|
1233
|
+
'ANTHROPIC_MODEL',
|
|
1234
|
+
'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY',
|
|
1235
|
+
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
1236
|
+
]);
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Resolve `~/.agents/` (the shared skills-registry directory).
|
|
1240
|
+
* Honors `process.env.AGENTS_DIR` so tests and operators can override
|
|
1241
|
+
* it without bouncing HOME.
|
|
1242
|
+
*/
|
|
1243
|
+
export function resolveAgentsDir() {
|
|
1244
|
+
if (process.env.AGENTS_DIR && process.env.AGENTS_DIR.trim()) {
|
|
1245
|
+
return process.env.AGENTS_DIR.trim();
|
|
1246
|
+
}
|
|
1247
|
+
return join(HOME, '.agents');
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** `~/.agents/` — shared skills-registry directory. Re-evaluated per read so
|
|
1251
|
+
* tests that flip `process.env.AGENTS_DIR` are honored. */
|
|
1252
|
+
export function AGENTS_DIR() {
|
|
1253
|
+
return resolveAgentsDir();
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Bizar-managed subdirectories of `~/.claude/`. Anything outside this
|
|
1258
|
+
* set under `~/.claude/` is treated as user-owned and is preserved
|
|
1259
|
+
* by `forceCleanInstall` (e.g. `.credentials.json`, `statsig/`,
|
|
1260
|
+
* `.playwright-mcp/`).
|
|
1261
|
+
*/
|
|
1262
|
+
const FORCE_CLEAN_WIPE_DIRS = Object.freeze([
|
|
1263
|
+
'agents',
|
|
1264
|
+
'skills',
|
|
1265
|
+
'commands',
|
|
1266
|
+
'hooks',
|
|
1267
|
+
'rules',
|
|
1268
|
+
'workflows',
|
|
1269
|
+
'plugins',
|
|
1270
|
+
]);
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* F-183 — wipe every Bizar-managed directory under `~/.claude/` plus
|
|
1274
|
+
* `~/.agents/`, back up `~/.claude/settings.json` env vars into
|
|
1275
|
+
* `process.env.BIZAR_SAVED_ENV`, and return a structured wipe report.
|
|
1276
|
+
*
|
|
1277
|
+
* The wipe is **idempotent** at the file-system level (rmSync is
|
|
1278
|
+
* recursive+force), and the resulting state is fully reproducible by
|
|
1279
|
+
* the subsequent `runProvision` call because the factory re-emits the
|
|
1280
|
+
* entire settings file from the shipped template (so the F-181
|
|
1281
|
+
* wildcard expansion in `permissions.allow` lands automatically).
|
|
1282
|
+
*
|
|
1283
|
+
* What is **not** wiped:
|
|
1284
|
+
* - `~/.config/bizar/` (BIZAR_HOME) — login state, telemetry,
|
|
1285
|
+
* worktree-queue, model picks, installed.json marker. Operators
|
|
1286
|
+
* rely on this surviving a forced reinstall.
|
|
1287
|
+
* - `~/.claude/.credentials.json`, `~/.claude/statsig/`,
|
|
1288
|
+
* `~/.claude/.playwright-mcp/` — third-party state managed by
|
|
1289
|
+
* Claude Code itself.
|
|
1290
|
+
* - Any subdirectory of `~/.claude/` not in FORCE_CLEAN_WIPE_DIRS.
|
|
1291
|
+
*
|
|
1292
|
+
* @param {{ dryRun?: boolean }} [opts]
|
|
1293
|
+
* @returns {{ ok: true, message: string, wiped: string[], preserved: string[], env: Record<string, string> }}
|
|
1294
|
+
*/
|
|
1295
|
+
export function forceCleanInstall(opts = {}) {
|
|
1296
|
+
const { dryRun = false } = opts;
|
|
1297
|
+
const claudeDir = resolveClaudeDir();
|
|
1298
|
+
const agentsDir = resolveAgentsDir();
|
|
1299
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
1300
|
+
|
|
1301
|
+
// 1. Stash existing env vars so `writeClaudeSettings` can re-inject
|
|
1302
|
+
// them when the freshly-emitted file lands. Use a string env so
|
|
1303
|
+
// we don't pollute the parent process object graph with arbitrary
|
|
1304
|
+
// operator-defined keys.
|
|
1305
|
+
let savedEnv = {};
|
|
1306
|
+
if (existsSync(settingsPath)) {
|
|
1307
|
+
try {
|
|
1308
|
+
const cur = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1309
|
+
if (cur && typeof cur === 'object' && cur.env && typeof cur.env === 'object') {
|
|
1310
|
+
for (const key of FORCE_CLEAN_PRESERVE_ENV_KEYS) {
|
|
1311
|
+
if (typeof cur.env[key] === 'string') savedEnv[key] = cur.env[key];
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
} catch { /* corrupt settings — treat as no env */ }
|
|
1315
|
+
}
|
|
1316
|
+
// Also pick up env values from the live process so a freshly-invoked
|
|
1317
|
+
// `bizar install --force` carries operator credentials through the
|
|
1318
|
+
// wipe even if the on-disk settings file is missing/stale.
|
|
1319
|
+
for (const key of FORCE_CLEAN_PRESERVE_ENV_KEYS) {
|
|
1320
|
+
if (!savedEnv[key] && process.env[key]) savedEnv[key] = process.env[key];
|
|
1321
|
+
}
|
|
1322
|
+
process.env.BIZAR_SAVED_ENV = JSON.stringify(savedEnv);
|
|
1323
|
+
|
|
1324
|
+
// 2. Wipe managed dirs.
|
|
1325
|
+
const wiped = [];
|
|
1326
|
+
for (const sub of FORCE_CLEAN_WIPE_DIRS) {
|
|
1327
|
+
const dir = join(claudeDir, sub);
|
|
1328
|
+
if (!existsSync(dir)) continue;
|
|
1329
|
+
wiped.push(dir);
|
|
1330
|
+
if (!dryRun) {
|
|
1331
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (existsSync(agentsDir)) {
|
|
1335
|
+
wiped.push(agentsDir);
|
|
1336
|
+
if (!dryRun) {
|
|
1337
|
+
try { rmSync(agentsDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// 3. Wipe settings.json so writeClaudeSettings re-emits from the
|
|
1341
|
+
// shipped template. Operators keep their gateway / auth env vars
|
|
1342
|
+
// via the BIZAR_SAVED_ENV stash.
|
|
1343
|
+
if (existsSync(settingsPath)) {
|
|
1344
|
+
wiped.push(settingsPath);
|
|
1345
|
+
if (!dryRun) {
|
|
1346
|
+
try { rmSync(settingsPath, { force: true }); } catch { /* ignore */ }
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const preserved = [];
|
|
1351
|
+
if (existsSync(BIZAR_HOME())) preserved.push(BIZAR_HOME());
|
|
1352
|
+
// Document the third-party state we intentionally left alone.
|
|
1353
|
+
for (const sub of ['.credentials.json', 'statsig', '.playwright-mcp']) {
|
|
1354
|
+
const p = join(claudeDir, sub);
|
|
1355
|
+
if (existsSync(p)) preserved.push(p);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const tag = dryRun ? '[dry-run] ' : '';
|
|
1359
|
+
const message = `${tag}F-183 clean: wiped ${wiped.length} paths; preserved ${preserved.length} paths (BIZAR_HOME + third-party state)`;
|
|
1360
|
+
return { ok: true, message, wiped, preserved, env: savedEnv };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Inverse of `forceCleanInstall` — clears the in-process stash once
|
|
1365
|
+
* `writeClaudeSettings` has consumed it. Idempotent and side-effect
|
|
1366
|
+
* free; exported so tests can run multiple forced installs without
|
|
1367
|
+
* leaking stale stashes between scenarios.
|
|
1368
|
+
*/
|
|
1369
|
+
export function clearSavedEnv() {
|
|
1370
|
+
delete process.env.BIZAR_SAVED_ENV;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1167
1373
|
// ─── CLI entry ──────────────────────────────────────────────────────────────
|
|
1168
1374
|
|
|
1169
1375
|
export function parseFlags(argv) {
|
|
@@ -1177,7 +1383,7 @@ export function parseFlags(argv) {
|
|
|
1177
1383
|
const v = argv[++i];
|
|
1178
1384
|
if (v === 'install' || v === 'update' || v === 'install-only-system') opts.mode = v;
|
|
1179
1385
|
} else if (a === '--dry-run') opts.dryRun = true;
|
|
1180
|
-
else if (a === '--force') opts.force = true;
|
|
1386
|
+
else if (a === '--force' || a === '--deep') opts.force = true;
|
|
1181
1387
|
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
1182
1388
|
else if (a === '--non-interactive') opts.yes = true;
|
|
1183
1389
|
else if (a === '--no-service') opts.start = false;
|
|
@@ -20,12 +20,12 @@ import { pathToFileURL } from 'node:url';
|
|
|
20
20
|
|
|
21
21
|
import { loadModelRouter } from '../../../config/agents/model-assignment.mjs';
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
function advise(reason) {
|
|
24
24
|
return {
|
|
25
25
|
hookSpecificOutput: {
|
|
26
26
|
hookEventName: 'PreToolUse',
|
|
27
|
-
permissionDecision: '
|
|
28
|
-
|
|
27
|
+
permissionDecision: 'allow',
|
|
28
|
+
additionalContext: `🟡 Model override guidance: ${reason} The dispatch will proceed regardless.`,
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -80,7 +80,7 @@ export async function guardAgentModel(input, options = {}) {
|
|
|
80
80
|
|
|
81
81
|
const allowed = configuredModels(registry);
|
|
82
82
|
if (!allowed.has(requested)) {
|
|
83
|
-
return
|
|
83
|
+
return advise(`Bizar Agent dispatch blocked: model override ${requested} is outside the configured dynamic tiers and the user-selected pool. Omit model to inherit the session or pick it via \`bizar models\`.`);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// User-selected models bypass live-discovery validation. The picker is the
|
|
@@ -89,7 +89,7 @@ export async function guardAgentModel(input, options = {}) {
|
|
|
89
89
|
if (!fromUserPick && Array.isArray(options.availableModelIds)) {
|
|
90
90
|
const available = new Set(options.availableModelIds);
|
|
91
91
|
if (!available.has(requested)) {
|
|
92
|
-
return
|
|
92
|
+
return advise(`Bizar Agent dispatch blocked: ${requested} was not reported by live discovery. Omit model to inherit the active session; do not retry aliases.`);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -65,8 +65,8 @@ process.stdin.on('end', () => {
|
|
|
65
65
|
process.stdout.write(JSON.stringify({
|
|
66
66
|
hookSpecificOutput: {
|
|
67
67
|
hookEventName: 'PreToolUse',
|
|
68
|
-
permissionDecision: '
|
|
69
|
-
|
|
68
|
+
permissionDecision: 'allow',
|
|
69
|
+
additionalContext: `🟡 Style suggestion: humanize the text before publishing. ${notes.join('; ')}. The write will proceed regardless.`,
|
|
70
70
|
},
|
|
71
71
|
}) + '\n');
|
|
72
72
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* NOT consume the marker — the hook chain may run more than once for a single
|
|
9
9
|
* bash invocation, and a one-shot rm caused sporadic false denies.
|
|
10
10
|
*
|
|
11
|
-
* Missing or stale markers
|
|
11
|
+
* Missing or stale markers emit an advisory reminder. Hard approval gates (commit,
|
|
12
12
|
* push, gh, publish, deploy) remain in git-workflow-guard.mjs.
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -94,8 +94,8 @@ process.stdin.on('end', () => {
|
|
|
94
94
|
process.stdout.write(JSON.stringify({
|
|
95
95
|
hookSpecificOutput: {
|
|
96
96
|
hookEventName: 'PreToolUse',
|
|
97
|
-
permissionDecision: '
|
|
98
|
-
|
|
97
|
+
permissionDecision: 'allow',
|
|
98
|
+
additionalContext: '🟡 /simplify not run on the current staged diff. Recommended: run /simplify, apply any justified cleanup, rerun tests, then retry the commit. The commit will proceed without /simplify if you choose.',
|
|
99
99
|
},
|
|
100
100
|
}) + '\n');
|
|
101
101
|
});
|
package/package.json
CHANGED
|
@@ -20,6 +20,12 @@ while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
|
|
|
20
20
|
if [ -z "$LOCAL_SHA" ] || [ "$LOCAL_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
21
21
|
continue
|
|
22
22
|
fi
|
|
23
|
+
# Tag pushes (refs/tags/*) point at existing commits; they don't introduce
|
|
24
|
+
# new file changes. Skip tag-only ref updates so an annotated tag pointing
|
|
25
|
+
# at a commit already pushed via master doesn't scan the full history.
|
|
26
|
+
case "$LOCAL_REF" in
|
|
27
|
+
refs/tags/*) continue ;;
|
|
28
|
+
esac
|
|
23
29
|
if [ -z "$REMOTE_SHA" ] || [ "$REMOTE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
24
30
|
# New branch — scan all commits on the local side
|
|
25
31
|
RANGE="$LOCAL_SHA"
|