@axiomatic-labs/claudeflow 2.13.24 → 2.13.26
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/lib/hook-overrides.js +18 -1
- package/lib/install.js +94 -3
- package/lib/panel.js +129 -4
- package/package.json +1 -1
package/lib/hook-overrides.js
CHANGED
|
@@ -51,7 +51,7 @@ function normalizeHandlerPath(command) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function defaultOverrides() {
|
|
54
|
-
return { version: 1, disabledHandlers: [], disabledReminders: [] };
|
|
54
|
+
return { version: 1, disabledHandlers: [], disabledReminders: [], enforcementDisabled: false };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function readOverrides(projectRoot) {
|
|
@@ -61,6 +61,7 @@ function readOverrides(projectRoot) {
|
|
|
61
61
|
version: data.version || 1,
|
|
62
62
|
disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
|
|
63
63
|
disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
|
|
64
|
+
enforcementDisabled: data.enforcementDisabled === true,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -173,6 +174,21 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
|
|
|
173
174
|
return { state: 'enabled' };
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
// Master enforcement switch. When OFF, the wrapper silences every hook
|
|
178
|
+
// listed in `.claude/hooks/shared/enforcement-catalog.js` regardless of
|
|
179
|
+
// per-handler manual toggles. Per-handler toggles outside the catalog
|
|
180
|
+
// remain in effect — they are independent layers.
|
|
181
|
+
function toggleEnforcementMode(projectRoot, { disable }) {
|
|
182
|
+
if (typeof disable !== 'boolean') return { state: 'error', reason: 'disable-required' };
|
|
183
|
+
const overrides = readOverrides(projectRoot);
|
|
184
|
+
if (overrides.enforcementDisabled === disable) {
|
|
185
|
+
return { state: 'noop', reason: disable ? 'already-off' : 'already-on' };
|
|
186
|
+
}
|
|
187
|
+
overrides.enforcementDisabled = disable;
|
|
188
|
+
writeOverrides(projectRoot, overrides);
|
|
189
|
+
return { state: disable ? 'off' : 'on' };
|
|
190
|
+
}
|
|
191
|
+
|
|
176
192
|
// Toggle a single reminder by id. Same persistence file, same hot-reload
|
|
177
193
|
// semantics — `reload-reminder.js` re-reads overrides on every invocation.
|
|
178
194
|
function toggleReminderOverride(projectRoot, { id, disable }) {
|
|
@@ -202,6 +218,7 @@ module.exports = {
|
|
|
202
218
|
writeOverrides,
|
|
203
219
|
toggleHookOverride,
|
|
204
220
|
toggleReminderOverride,
|
|
221
|
+
toggleEnforcementMode,
|
|
205
222
|
normalizeHandlerPath,
|
|
206
223
|
matchesOverride,
|
|
207
224
|
};
|
package/lib/install.js
CHANGED
|
@@ -334,6 +334,7 @@ function ensureGlobalCli(version) {
|
|
|
334
334
|
autoInstalled: !needsUpgrade,
|
|
335
335
|
upgraded: needsUpgrade,
|
|
336
336
|
fromVersion: installedVersion || undefined,
|
|
337
|
+
shellPath: verifyShellCanFindCli(),
|
|
337
338
|
};
|
|
338
339
|
}
|
|
339
340
|
return {
|
|
@@ -368,6 +369,67 @@ function readGlobalCliVersion() {
|
|
|
368
369
|
}
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
// Returns { ok: boolean | null, binPath?: string, shellRcFile?: string,
|
|
373
|
+
// nvmDetected?: boolean, suggestedFix?: string }
|
|
374
|
+
//
|
|
375
|
+
// Distinguishes "package installed globally" from "package findable from
|
|
376
|
+
// the user's interactive shell". Catches the common nvm-lazy-load setup
|
|
377
|
+
// where `npm install -g` writes a binary the shell can't see until nvm
|
|
378
|
+
// is sourced — typically discovered only when the user opens a fresh
|
|
379
|
+
// terminal and types `claudeflow` and gets "command not found".
|
|
380
|
+
function verifyShellCanFindCli() {
|
|
381
|
+
// Skip on Windows; lazy-load + nvm-windows behave differently and the
|
|
382
|
+
// login-shell trick below is brittle there.
|
|
383
|
+
if (process.platform === 'win32') return { ok: null };
|
|
384
|
+
|
|
385
|
+
const shellPath = process.env.SHELL;
|
|
386
|
+
if (!shellPath || !fs.existsSync(shellPath)) return { ok: null };
|
|
387
|
+
|
|
388
|
+
// Run a non-inherited interactive login shell — that's the closest
|
|
389
|
+
// simulation of "user opens a new terminal" without polluting the
|
|
390
|
+
// current process.
|
|
391
|
+
let foundOnUserPath = null;
|
|
392
|
+
try {
|
|
393
|
+
const out = execSync(`${shellPath} -lic 'command -v claudeflow 2>/dev/null'`, {
|
|
394
|
+
encoding: 'utf8',
|
|
395
|
+
timeout: 6000,
|
|
396
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
397
|
+
}).trim();
|
|
398
|
+
foundOnUserPath = out.length > 0;
|
|
399
|
+
} catch {
|
|
400
|
+
// Some shells exit non-zero when `command -v` finds nothing — treat
|
|
401
|
+
// that as "not found" rather than as an inconclusive check.
|
|
402
|
+
foundOnUserPath = false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (foundOnUserPath) return { ok: true };
|
|
406
|
+
|
|
407
|
+
// Resolve where the binary actually lives so the warning is concrete.
|
|
408
|
+
let binPath = null;
|
|
409
|
+
try {
|
|
410
|
+
const prefix = execSync('npm config get prefix', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
411
|
+
binPath = path.join(prefix, 'bin', 'claudeflow');
|
|
412
|
+
} catch {}
|
|
413
|
+
|
|
414
|
+
const home = process.env.HOME || '';
|
|
415
|
+
const nvmDetected = !!process.env.NVM_DIR
|
|
416
|
+
|| fs.existsSync(path.join(home, '.nvm', 'nvm.sh'));
|
|
417
|
+
const isZsh = shellPath.endsWith('/zsh');
|
|
418
|
+
const shellRcFile = isZsh ? '~/.zshrc' : (shellPath.endsWith('/bash') ? '~/.bashrc' : '~/.profile');
|
|
419
|
+
|
|
420
|
+
// Wrapper-function fix that mirrors the user's existing nvm lazy-load
|
|
421
|
+
// pattern (e.g., npx, node, npm). Single line, paste into rcFile, done.
|
|
422
|
+
const wrapperFix = `claudeflow() { unset -f claudeflow; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; claudeflow "$@"; }`;
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
binPath,
|
|
427
|
+
shellRcFile,
|
|
428
|
+
nvmDetected,
|
|
429
|
+
suggestedFix: wrapperFix,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
371
433
|
function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled: false }) {
|
|
372
434
|
const MANIFESTS = ['package.json', 'pyproject.toml', 'Gemfile', 'go.mod', 'Cargo.toml', 'composer.json'];
|
|
373
435
|
const SKIP_DIRS = new Set(['node_modules', 'vendor', '__pycache__', 'dist', 'build', '.next', '.nuxt', '.output', '.claude']);
|
|
@@ -401,7 +463,27 @@ function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled:
|
|
|
401
463
|
` ${ui.DIM}✓ Upgraded global ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} CLI${cliStatus.fromVersion ? ` from ${cliStatus.fromVersion}` : ''}.${ui.RESET}`
|
|
402
464
|
);
|
|
403
465
|
console.log('');
|
|
404
|
-
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Shell PATH warning — if the global package was installed but the
|
|
469
|
+
// user's interactive shell can't find the binary (typical with
|
|
470
|
+
// nvm lazy-load), emit a concrete fix.
|
|
471
|
+
if (cliStatus.shellPath && cliStatus.shellPath.ok === false) {
|
|
472
|
+
const sp = cliStatus.shellPath;
|
|
473
|
+
console.log(` ${ui.YELLOW}⚠ ${ui.BOLD}claudeflow${ui.RESET}${ui.YELLOW} CLI installed but NOT on your shell PATH.${ui.RESET}`);
|
|
474
|
+
if (sp.binPath) {
|
|
475
|
+
console.log(` ${ui.DIM}Binary lives at: ${sp.binPath}${ui.RESET}`);
|
|
476
|
+
}
|
|
477
|
+
if (sp.nvmDetected) {
|
|
478
|
+
console.log(` ${ui.DIM}Likely cause: nvm lazy-load. Add this to ${sp.shellRcFile}, then reopen terminal:${ui.RESET}`);
|
|
479
|
+
console.log(` ${ui.CYAN}${sp.suggestedFix}${ui.RESET}`);
|
|
480
|
+
} else {
|
|
481
|
+
console.log(` ${ui.DIM}Add the npm prefix bin directory to your PATH in ${sp.shellRcFile}.${ui.RESET}`);
|
|
482
|
+
}
|
|
483
|
+
console.log('');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (!cliStatus.available) {
|
|
405
487
|
console.log(` ${ui.DIM}To get the ${ui.CYAN}claudeflow${ui.RESET}${ui.DIM} shell command, run:${ui.RESET}`);
|
|
406
488
|
console.log(` ${ui.CYAN}npm i -g @axiomatic-labs/claudeflow${ui.RESET}`);
|
|
407
489
|
if (cliStatus.error && cliStatus.error !== 'skipped') {
|
|
@@ -434,14 +516,21 @@ function showGettingStarted(cwd, cliStatus = { available: false, autoInstalled:
|
|
|
434
516
|
}
|
|
435
517
|
|
|
436
518
|
console.log('');
|
|
437
|
-
console.log(` ${ui.BOLD}
|
|
519
|
+
console.log(` ${ui.BOLD}Slash commands (inside Claude Code):${ui.RESET}`);
|
|
438
520
|
console.log(` ${ui.CYAN}/claudeflow-install${ui.RESET} ${ui.DIM}Route setup automatically for new or existing projects${ui.RESET}`);
|
|
439
521
|
console.log(` ${ui.CYAN}/claudeflow-build${ui.RESET} ${ui.DIM}Execute an approved spec for larger or higher-risk changes${ui.RESET}`);
|
|
440
522
|
console.log(` ${ui.CYAN}/claudeflow-design-tokens${ui.RESET} ${ui.DIM}Create design reference from a site or screenshot${ui.RESET}`);
|
|
441
523
|
console.log(` ${ui.CYAN}/claudeflow-create-ui${ui.RESET} ${ui.DIM}Build pages from visual references${ui.RESET}`);
|
|
442
524
|
console.log(` ${ui.CYAN}/claudeflow-checkpoints${ui.RESET} ${ui.DIM}Auto-save work with git checkpoints${ui.RESET}`);
|
|
443
525
|
console.log(` ${ui.CYAN}/claudeflow-add-skill${ui.RESET} ${ui.DIM}Add new technology skills (e.g. /claudeflow-add-skill stripe)${ui.RESET}`);
|
|
444
|
-
console.log(` ${ui.CYAN}/claudeflow-add-tools${ui.RESET}
|
|
526
|
+
console.log(` ${ui.CYAN}/claudeflow-add-tools${ui.RESET} ${ui.DIM}Discover and install MCP servers + CLI tools${ui.RESET}`);
|
|
527
|
+
console.log('');
|
|
528
|
+
console.log(` ${ui.BOLD}Shell commands (outside Claude Code):${ui.RESET}`);
|
|
529
|
+
console.log(` ${ui.CYAN}claudeflow${ui.RESET} ${ui.DIM}Start Claude Code with this project's claudeflow context${ui.RESET}`);
|
|
530
|
+
console.log(` ${ui.CYAN}claudeflow panel${ui.RESET} ${ui.DIM}Open the local web dashboard — toggle hooks, reminders, enforcement${ui.RESET}`);
|
|
531
|
+
console.log(` ${ui.CYAN}claudeflow doctor${ui.RESET} ${ui.DIM}Diagnose CDP-port and lockfile issues; ${ui.CYAN}--fix${ui.RESET}${ui.DIM} to auto-repair${ui.RESET}`);
|
|
532
|
+
console.log(` ${ui.CYAN}claudeflow install${ui.RESET} ${ui.DIM}Refresh the template + global CLI in this project${ui.RESET}`);
|
|
533
|
+
console.log(` ${ui.CYAN}claudeflow version${ui.RESET} ${ui.DIM}Show installed framework + CLI versions${ui.RESET}`);
|
|
445
534
|
console.log('');
|
|
446
535
|
console.log(` ${ui.DIM}Docs: https://claudeflow.dev${ui.RESET}`);
|
|
447
536
|
console.log('');
|
|
@@ -1107,4 +1196,6 @@ module.exports = Object.assign(run, {
|
|
|
1107
1196
|
mergeClaudeSettings,
|
|
1108
1197
|
commandExists,
|
|
1109
1198
|
readGlobalCliVersion,
|
|
1199
|
+
verifyShellCanFindCli,
|
|
1200
|
+
showGettingStarted,
|
|
1110
1201
|
});
|
package/lib/panel.js
CHANGED
|
@@ -25,9 +25,21 @@ const {
|
|
|
25
25
|
readOverrides,
|
|
26
26
|
toggleHookOverride,
|
|
27
27
|
toggleReminderOverride,
|
|
28
|
+
toggleEnforcementMode,
|
|
28
29
|
normalizeHandlerPath,
|
|
29
30
|
} = require('./hook-overrides.js');
|
|
30
31
|
|
|
32
|
+
function loadEnforcementCatalog(cwd) {
|
|
33
|
+
const p = path.join(cwd, '.claude', 'hooks', 'shared', 'enforcement-catalog.js');
|
|
34
|
+
if (!fs.existsSync(p)) return null;
|
|
35
|
+
try {
|
|
36
|
+
delete require.cache[require.resolve(p)];
|
|
37
|
+
return require(p);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
// Lazy-load the reminder catalog from the template hook helper. The path
|
|
32
44
|
// resolves the same way at install time and from the template repo.
|
|
33
45
|
function loadReminderHelpers(cwd) {
|
|
@@ -112,6 +124,11 @@ function getHooksInfo(cwd) {
|
|
|
112
124
|
for (const d of overrides.disabledHandlers || []) {
|
|
113
125
|
disabledIndex.set(`${d.event}|${d.matcher || ''}|${d.handler}`, d);
|
|
114
126
|
}
|
|
127
|
+
const catalog = loadEnforcementCatalog(cwd);
|
|
128
|
+
const isEnforcement = catalog && typeof catalog.isEnforcementHook === 'function'
|
|
129
|
+
? catalog.isEnforcementHook
|
|
130
|
+
: () => false;
|
|
131
|
+
const enforcementDisabled = overrides.enforcementDisabled === true;
|
|
115
132
|
|
|
116
133
|
// settings.json lists every hook (with each command wrapped through
|
|
117
134
|
// run-with-override.js). The wrapper checks overrides at invocation time,
|
|
@@ -133,14 +150,21 @@ function getHooksInfo(cwd) {
|
|
|
133
150
|
const isEmpty = !handlerPath;
|
|
134
151
|
if (isEmpty) warnings++;
|
|
135
152
|
const key = `${event}|${matcher}|${handlerPath}`;
|
|
136
|
-
const
|
|
153
|
+
const enforcement = isEnforcement(event, matcher, handlerPath);
|
|
154
|
+
const masterDisabled = enforcement && enforcementDisabled;
|
|
155
|
+
const manualDisabled = disabledIndex.has(key);
|
|
156
|
+
const isDisabled = manualDisabled || masterDisabled;
|
|
137
157
|
if (isDisabled) disabledCount++;
|
|
138
158
|
handlers.push({
|
|
139
159
|
handler: handlerPath || '(empty command)',
|
|
140
160
|
matcher,
|
|
141
161
|
matcherDisplay: matcher || '(any)',
|
|
142
162
|
empty: isEmpty,
|
|
163
|
+
enforcement,
|
|
143
164
|
disabled: isDisabled,
|
|
165
|
+
disabledBy: masterDisabled && !manualDisabled ? 'enforcement-master'
|
|
166
|
+
: manualDisabled ? 'manual'
|
|
167
|
+
: null,
|
|
144
168
|
rawCommand: cmd,
|
|
145
169
|
});
|
|
146
170
|
totalHandlers++;
|
|
@@ -244,6 +268,24 @@ function getActiveRunInfo(cwd) {
|
|
|
244
268
|
};
|
|
245
269
|
}
|
|
246
270
|
|
|
271
|
+
function getEnforcementInfo(cwd) {
|
|
272
|
+
const catalog = loadEnforcementCatalog(cwd);
|
|
273
|
+
const overrides = readOverrides(cwd);
|
|
274
|
+
if (!catalog || !Array.isArray(catalog.ENFORCEMENT_HOOKS)) {
|
|
275
|
+
return { available: false, on: !overrides.enforcementDisabled, count: 0 };
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
available: true,
|
|
279
|
+
on: !overrides.enforcementDisabled,
|
|
280
|
+
count: catalog.ENFORCEMENT_HOOKS.length,
|
|
281
|
+
hooks: catalog.ENFORCEMENT_HOOKS.map((h) => ({
|
|
282
|
+
event: h.event,
|
|
283
|
+
handler: h.handler,
|
|
284
|
+
matcher: h.matcher,
|
|
285
|
+
})),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
247
289
|
function getRemindersInfo(cwd) {
|
|
248
290
|
const helpers = loadReminderHelpers(cwd);
|
|
249
291
|
if (!helpers || typeof helpers.listReminderCatalog !== 'function') {
|
|
@@ -288,6 +330,7 @@ function collectStatus(cwd) {
|
|
|
288
330
|
setupContext: getSetupContextInfo(cwd),
|
|
289
331
|
activeRun: getActiveRunInfo(cwd),
|
|
290
332
|
reminders: getRemindersInfo(cwd),
|
|
333
|
+
enforcement: getEnforcementInfo(cwd),
|
|
291
334
|
doctor: getDoctorInfo(cwd),
|
|
292
335
|
};
|
|
293
336
|
}
|
|
@@ -326,6 +369,19 @@ header .cwd { color: var(--muted); font-family: var(--mono); font-size: 12px; fl
|
|
|
326
369
|
header .actions { display: flex; gap: 12px; align-items: center; color: var(--muted); font-size: 12px; }
|
|
327
370
|
header button { background: var(--panel-2); border: 1px solid var(--border); color: var(--fg); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
|
|
328
371
|
header button:hover { border-color: var(--accent); }
|
|
372
|
+
.enforce-pill { display: inline-flex; align-items: center; gap: 8px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--panel-2); font-size: 12px; transition: all 0.2s; }
|
|
373
|
+
.enforce-pill .enforce-label { color: var(--muted); font-family: var(--mono); letter-spacing: 0.5px; text-transform: uppercase; font-size: 11px; }
|
|
374
|
+
.enforce-pill .enforce-state { font-family: var(--mono); font-weight: 600; font-size: 12px; min-width: 26px; text-align: left; }
|
|
375
|
+
.enforce-pill[data-state="on"] { border-color: rgba(63, 185, 80, 0.5); background: rgba(63, 185, 80, 0.08); }
|
|
376
|
+
.enforce-pill[data-state="on"] .enforce-state { color: var(--ok); }
|
|
377
|
+
.enforce-pill[data-state="off"] { border-color: rgba(248, 81, 73, 0.6); background: rgba(248, 81, 73, 0.12); box-shadow: 0 0 0 1px rgba(248, 81, 73, 0.3); }
|
|
378
|
+
.enforce-pill[data-state="off"] .enforce-state { color: var(--err); }
|
|
379
|
+
.enforce-switch { position: relative; display: inline-block; width: 32px; height: 18px; }
|
|
380
|
+
.enforce-switch input { opacity: 0; width: 0; height: 0; }
|
|
381
|
+
.enforce-slider { position: absolute; cursor: pointer; inset: 0; background: var(--border); border-radius: 999px; transition: background 0.2s; }
|
|
382
|
+
.enforce-slider::before { content: ""; position: absolute; height: 14px; width: 14px; left: 2px; top: 2px; background: var(--fg); border-radius: 50%; transition: transform 0.2s; }
|
|
383
|
+
.enforce-switch input:checked + .enforce-slider { background: var(--ok); }
|
|
384
|
+
.enforce-switch input:checked + .enforce-slider::before { transform: translateX(14px); }
|
|
329
385
|
main { display: flex; min-height: calc(100vh - 56px); }
|
|
330
386
|
nav { width: 220px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
|
|
331
387
|
nav a { display: flex; align-items: center; gap: 10px; padding: 10px 22px; color: var(--fg); text-decoration: none; cursor: pointer; border-left: 3px solid transparent; font-size: 13px; }
|
|
@@ -397,6 +453,11 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
397
453
|
<span class="version" id="version">…</span>
|
|
398
454
|
<span class="cwd" id="cwd">…</span>
|
|
399
455
|
<span class="actions">
|
|
456
|
+
<span class="enforce-pill" id="enforce-pill" title="Master switch — toggles every blocking enforcement hook (freeze-step-contract, enforce-task-creation, stack-debt-guard, ecosystem-artifacts, workflow-enforcement). Validators and reminders are unaffected.">
|
|
457
|
+
<span class="enforce-label">Enforcement</span>
|
|
458
|
+
<label class="enforce-switch"><input type="checkbox" id="enforce-toggle" /><span class="enforce-slider"></span></label>
|
|
459
|
+
<span class="enforce-state" id="enforce-state">…</span>
|
|
460
|
+
</span>
|
|
400
461
|
<label class="toggle"><input type="checkbox" id="auto" checked /> auto-refresh 5s</label>
|
|
401
462
|
<button id="refresh">Refresh</button>
|
|
402
463
|
</span>
|
|
@@ -486,6 +547,21 @@ async function refresh() {
|
|
|
486
547
|
function renderHeader() {
|
|
487
548
|
document.getElementById('version').textContent = 'v' + state.version.installed;
|
|
488
549
|
document.getElementById('cwd').textContent = state.cwd;
|
|
550
|
+
const ef = state.enforcement;
|
|
551
|
+
const pill = document.getElementById('enforce-pill');
|
|
552
|
+
const cb = document.getElementById('enforce-toggle');
|
|
553
|
+
const lbl = document.getElementById('enforce-state');
|
|
554
|
+
if (ef && ef.available) {
|
|
555
|
+
pill.style.display = '';
|
|
556
|
+
pill.dataset.state = ef.on ? 'on' : 'off';
|
|
557
|
+
cb.checked = ef.on;
|
|
558
|
+
lbl.textContent = ef.on ? 'ON' : 'OFF';
|
|
559
|
+
pill.title = ef.on
|
|
560
|
+
? 'Enforcement is ON — ' + ef.count + ' blocking hook(s) active. Click to disable all.'
|
|
561
|
+
: 'Enforcement is OFF — ' + ef.count + ' blocking hook(s) silenced. Reminders + validators still run.';
|
|
562
|
+
} else {
|
|
563
|
+
pill.style.display = 'none';
|
|
564
|
+
}
|
|
489
565
|
}
|
|
490
566
|
|
|
491
567
|
function severityFor(id) {
|
|
@@ -563,6 +639,9 @@ function renderOverview() {
|
|
|
563
639
|
const rm = s.reminders && s.reminders.available
|
|
564
640
|
? \`\${s.reminders.activeCount} active · \${s.reminders.disabledCount} disabled\`
|
|
565
641
|
: 'unavailable';
|
|
642
|
+
const ef = s.enforcement && s.enforcement.available
|
|
643
|
+
? (s.enforcement.on ? \`ON (\${s.enforcement.count} blocking hooks)\` : \`OFF — \${s.enforcement.count} blocking hooks silenced\`)
|
|
644
|
+
: 'unavailable';
|
|
566
645
|
return \`<h2>Overview</h2>
|
|
567
646
|
<p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
|
|
568
647
|
<div class="card">
|
|
@@ -573,6 +652,7 @@ function renderOverview() {
|
|
|
573
652
|
\${row('Setup context', sc, { kind: s.setupContext.exists ? (s.setupContext.toolingComplete ? 'ok' : 'warn') : 'err', text: s.setupContext.exists ? (s.setupContext.toolingComplete ? '✓' : '!') : '✗' })}
|
|
574
653
|
\${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
|
|
575
654
|
\${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
|
|
655
|
+
\${row('Enforcement', ef, { kind: s.enforcement && s.enforcement.available ? (s.enforcement.on ? 'ok' : 'err') : 'err', text: s.enforcement && s.enforcement.available ? (s.enforcement.on ? '✓' : '✗') : '·' })}
|
|
576
656
|
\${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
|
|
577
657
|
</div>\`;
|
|
578
658
|
}
|
|
@@ -603,10 +683,13 @@ function renderHooks() {
|
|
|
603
683
|
const cls = hd.disabled ? 'handler disabled-row' : 'handler';
|
|
604
684
|
const checked = hd.disabled ? '' : 'checked';
|
|
605
685
|
const dataset = \`data-event="\${escapeHtml(ev.event)}" data-matcher="\${escapeHtml(hd.matcher)}" data-handler="\${escapeHtml(hd.handler)}"\`;
|
|
606
|
-
const
|
|
686
|
+
const enforcementBadge = hd.enforcement ? '<span class="badge warn" title="Member of the enforcement catalog — affected by the master switch">enforcement</span>' : '';
|
|
687
|
+
const masterBadge = hd.disabledBy === 'enforcement-master' ? '<span class="badge err" title="Silenced by the master Enforcement OFF switch in the header">master OFF</span>' : '';
|
|
688
|
+
const manualBadge = hd.disabledBy === 'manual' ? '<span class="badge info">disabled</span>' : '';
|
|
689
|
+
const lockedByMaster = hd.disabledBy === 'enforcement-master';
|
|
607
690
|
return \`<div class="\${cls}">
|
|
608
|
-
<label class="toggle"><input type="checkbox" class="hook-toggle" \${dataset} \${checked} \${hd.empty ? 'disabled' : ''} />
|
|
609
|
-
<span>\${escapeHtml(hd.handler)} \${
|
|
691
|
+
<label class="toggle"><input type="checkbox" class="hook-toggle" \${dataset} \${checked} \${hd.empty || lockedByMaster ? 'disabled' : ''} />
|
|
692
|
+
<span>\${escapeHtml(hd.handler)} \${enforcementBadge} \${masterBadge} \${manualBadge} \${mark}</span></label>
|
|
610
693
|
<span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
|
|
611
694
|
</div>\`;
|
|
612
695
|
}).join('');
|
|
@@ -773,6 +856,34 @@ document.addEventListener('toggle', (e) => {
|
|
|
773
856
|
}
|
|
774
857
|
}, true);
|
|
775
858
|
|
|
859
|
+
async function toggleEnforcement(disable) {
|
|
860
|
+
try {
|
|
861
|
+
const r = await fetch('/api/enforcement/toggle', {
|
|
862
|
+
method: 'POST',
|
|
863
|
+
headers: { 'Content-Type': 'application/json' },
|
|
864
|
+
body: JSON.stringify({ disable }),
|
|
865
|
+
});
|
|
866
|
+
const result = await r.json();
|
|
867
|
+
if (!r.ok || result.state === 'error') {
|
|
868
|
+
showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
|
|
869
|
+
} else if (result.state === 'noop') {
|
|
870
|
+
showToast('Enforcement already in target state', 'ok');
|
|
871
|
+
} else {
|
|
872
|
+
showToast(disable
|
|
873
|
+
? 'Enforcement OFF — blocking hooks silenced. Validators + reminders still run.'
|
|
874
|
+
: 'Enforcement ON — full blocking restored.',
|
|
875
|
+
disable ? 'err' : 'ok');
|
|
876
|
+
}
|
|
877
|
+
} catch (e) {
|
|
878
|
+
showToast('Network error: ' + e.message, 'err');
|
|
879
|
+
}
|
|
880
|
+
await refresh();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
document.getElementById('enforce-toggle').onchange = (e) => {
|
|
884
|
+
toggleEnforcement(!e.target.checked);
|
|
885
|
+
};
|
|
886
|
+
|
|
776
887
|
document.getElementById('refresh').onclick = refresh;
|
|
777
888
|
document.getElementById('auto').onchange = (e) => {
|
|
778
889
|
if (timer) clearInterval(timer);
|
|
@@ -842,6 +953,20 @@ function handler(cwd) {
|
|
|
842
953
|
return send(status, JSON.stringify(result), 'application/json');
|
|
843
954
|
}
|
|
844
955
|
|
|
956
|
+
if (req.method === 'POST' && route === '/api/enforcement/toggle') {
|
|
957
|
+
const raw = await readRequestBody(req);
|
|
958
|
+
let payload;
|
|
959
|
+
try { payload = JSON.parse(raw); }
|
|
960
|
+
catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
|
|
961
|
+
const { disable } = payload;
|
|
962
|
+
if (typeof disable !== 'boolean') {
|
|
963
|
+
return send(400, JSON.stringify({ error: 'disable required' }), 'application/json');
|
|
964
|
+
}
|
|
965
|
+
const result = toggleEnforcementMode(cwd, { disable });
|
|
966
|
+
const status = result.state === 'error' ? 500 : 200;
|
|
967
|
+
return send(status, JSON.stringify(result), 'application/json');
|
|
968
|
+
}
|
|
969
|
+
|
|
845
970
|
if (req.method === 'POST' && route === '/api/reminders/toggle') {
|
|
846
971
|
const raw = await readRequestBody(req);
|
|
847
972
|
let payload;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.26",
|
|
4
4
|
"description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claudeflow": "./bin/cli.js"
|