@connexum/ai-governance 1.0.0-beta.15 → 1.0.0-beta.16
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/dist/cli/index.d.ts +36 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +230 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/esm/cli/index.js +228 -9
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/hooks/audit-logger.sh +21 -7
- package/dist/hooks/cost-budget-gate.sh +1 -1
- package/dist/hooks/destructive-command-guard.sh +1 -1
- package/dist/hooks/file-boundary-guard.sh +1 -1
- package/dist/hooks/file-change-tracker.sh +1 -1
- package/dist/hooks/governance-file-shield.sh +1 -1
- package/dist/hooks/hook-utils.sh +1 -1
- package/dist/hooks/output-exfiltration-scanner.sh +1 -1
- package/dist/hooks/secret-leak-scanner.sh +1 -1
- package/dist/hooks/token-tracker.sh +1 -1
- package/dist/hooks/web-access-gate.sh +1 -1
- package/package.json +1 -1
- package/src/hooks/audit-logger.sh +21 -7
- package/src/hooks/cost-budget-gate.sh +1 -1
- package/src/hooks/destructive-command-guard.sh +1 -1
- package/src/hooks/file-boundary-guard.sh +1 -1
- package/src/hooks/file-change-tracker.sh +1 -1
- package/src/hooks/governance-file-shield.sh +1 -1
- package/src/hooks/hook-utils.sh +1 -1
- package/src/hooks/output-exfiltration-scanner.sh +1 -1
- package/src/hooks/secret-leak-scanner.sh +1 -1
- package/src/hooks/token-tracker.sh +1 -1
- package/src/hooks/web-access-gate.sh +1 -1
package/dist/cli/index.d.ts
CHANGED
|
@@ -116,10 +116,46 @@ export interface GovernanceJsonConfig {
|
|
|
116
116
|
*/
|
|
117
117
|
export declare function readLicenseServerUrl(projectDir: string): string;
|
|
118
118
|
export declare function generateConfig(packs: string[], license: LicenseInfo, licenseServerUrl?: string, runtime?: GovernanceJsonConfig['runtime']): GovernanceJsonConfig;
|
|
119
|
+
/**
|
|
120
|
+
* P0 install-safety guard. Governance is a PROJECT-LOCAL install: hooks are
|
|
121
|
+
* written to `<projectDir>/.claude/settings.json`. If `init` runs from the
|
|
122
|
+
* home directory, that path resolves to the GLOBAL Claude Code config
|
|
123
|
+
* (`~/.claude/settings.json`) and the hooks fire in EVERY project on the
|
|
124
|
+
* machine — the root cause of the 2026-06-03 global-hook-hang incident, where
|
|
125
|
+
* audit-logger.sh stacked up and froze Claude Code across all terminals.
|
|
126
|
+
*
|
|
127
|
+
* Throws an actionable Error if `projectDir` is the home directory, or if
|
|
128
|
+
* `<projectDir>/.claude` is the global Claude config dir (honoring a relocated
|
|
129
|
+
* CLAUDE_CONFIG_DIR). The CLI handler converts the throw into a clean
|
|
130
|
+
* stderr message + exit 1; calling installHooks directly still fails closed.
|
|
131
|
+
*/
|
|
132
|
+
export declare function assertSafeInstallDir(projectDir: string): void;
|
|
119
133
|
export declare function installHooks(projectDir: string, config: GovernanceJsonConfig): {
|
|
120
134
|
installed: number;
|
|
121
135
|
errors: string[];
|
|
122
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* Clean removal escape hatch for `ai-governance uninstall`.
|
|
139
|
+
*
|
|
140
|
+
* The 2026-06-03 incident had NO uninstall path, so recovery meant manually
|
|
141
|
+
* killing processes and `rm -rf`. This:
|
|
142
|
+
* 1. strips ONLY our hooks from `<projectDir>/.claude/settings.json`
|
|
143
|
+
* (identified by a command referencing `<projectDir>/.governance/hooks/`),
|
|
144
|
+
* preserving any hooks the user added themselves;
|
|
145
|
+
* 2. removes the generated hook scripts and `.governance.json`;
|
|
146
|
+
* 3. PRESERVES the tamper-evident audit chain (`.governance/audit/`) by
|
|
147
|
+
* default — it is compliance evidence — deleting it only when `purge` is
|
|
148
|
+
* set.
|
|
149
|
+
*
|
|
150
|
+
* Idempotent and non-throwing on a project with nothing installed.
|
|
151
|
+
*/
|
|
152
|
+
export declare function uninstallGovernance(projectDir: string, opts?: {
|
|
153
|
+
purge?: boolean;
|
|
154
|
+
}): {
|
|
155
|
+
hooksRemoved: number;
|
|
156
|
+
removedPaths: string[];
|
|
157
|
+
auditPreserved: string | null;
|
|
158
|
+
};
|
|
123
159
|
/** Structured aiIntegrations block in the nonInteractiveInit JSON output. */
|
|
124
160
|
export interface AiIntegrationsResult {
|
|
125
161
|
claudeCode: {
|
package/dist/cli/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -55,7 +55,9 @@ exports.detectIde = detectIde;
|
|
|
55
55
|
exports.validateLegacyLicense = validateLegacyLicense;
|
|
56
56
|
exports.readLicenseServerUrl = readLicenseServerUrl;
|
|
57
57
|
exports.generateConfig = generateConfig;
|
|
58
|
+
exports.assertSafeInstallDir = assertSafeInstallDir;
|
|
58
59
|
exports.installHooks = installHooks;
|
|
60
|
+
exports.uninstallGovernance = uninstallGovernance;
|
|
59
61
|
exports.writeClaudeSlashCommands = writeClaudeSlashCommands;
|
|
60
62
|
exports.writeGovernanceScannerAgent = writeGovernanceScannerAgent;
|
|
61
63
|
exports.writeCursorGovernanceRules = writeCursorGovernanceRules;
|
|
@@ -68,6 +70,7 @@ exports.verifyChain = verifyChain;
|
|
|
68
70
|
exports.generateAutowrapArtifacts = generateAutowrapArtifacts;
|
|
69
71
|
const fs = __importStar(require("fs"));
|
|
70
72
|
const path = __importStar(require("path"));
|
|
73
|
+
const os = __importStar(require("os"));
|
|
71
74
|
const crypto = __importStar(require("crypto"));
|
|
72
75
|
const readline = __importStar(require("readline"));
|
|
73
76
|
const https = __importStar(require("https"));
|
|
@@ -311,6 +314,31 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
311
314
|
const HOOK_EXT = IS_WINDOWS ? '.ps1' : '.sh';
|
|
312
315
|
const HOOK_RUNNER = IS_WINDOWS ? 'pwsh' : 'bash';
|
|
313
316
|
const HOOK_SUBDIR = IS_WINDOWS ? 'powershell' : '';
|
|
317
|
+
// P0 no-hang bound. Every generated hook carries this timeout (seconds) so a
|
|
318
|
+
// hook can never block the agent's tool call — the 2026-06-03 incident had a
|
|
319
|
+
// hung audit-logger.sh stack up for >1h because the entries had no timeout.
|
|
320
|
+
// Enforced two ways: the host honors the `timeout` field, and where a coreutils
|
|
321
|
+
// timeout binary exists the command self-limits via `timeout -k`. Honors
|
|
322
|
+
// Invariant 2 (advisory, never in the synchronous critical path).
|
|
323
|
+
const HOOK_TIMEOUT_SECONDS = 10;
|
|
324
|
+
// Resolve a coreutils timeout binary present on this machine: GNU `timeout`
|
|
325
|
+
// (Linux) or `gtimeout` (macOS + coreutils). Returns null on Windows or when
|
|
326
|
+
// neither exists (a default macOS) — callers must then NOT wrap the command in
|
|
327
|
+
// it, or the hook would fail with "command not found" and silently not run.
|
|
328
|
+
function resolveTimeoutBin() {
|
|
329
|
+
if (IS_WINDOWS)
|
|
330
|
+
return null;
|
|
331
|
+
const { spawnSync } = require('node:child_process');
|
|
332
|
+
for (const bin of ['timeout', 'gtimeout']) {
|
|
333
|
+
try {
|
|
334
|
+
if (spawnSync('sh', ['-c', `command -v ${bin}`], { stdio: 'ignore' }).status === 0) {
|
|
335
|
+
return bin;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch { /* fall through */ }
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
314
342
|
function detectIde(projectDir) {
|
|
315
343
|
const markers = [
|
|
316
344
|
{ dir: '.claude', ide: 'claude-code' },
|
|
@@ -508,7 +536,52 @@ function generateConfig(packs, license, licenseServerUrl, runtime) {
|
|
|
508
536
|
};
|
|
509
537
|
}
|
|
510
538
|
// --- Hook installation ---
|
|
539
|
+
/**
|
|
540
|
+
* P0 install-safety guard. Governance is a PROJECT-LOCAL install: hooks are
|
|
541
|
+
* written to `<projectDir>/.claude/settings.json`. If `init` runs from the
|
|
542
|
+
* home directory, that path resolves to the GLOBAL Claude Code config
|
|
543
|
+
* (`~/.claude/settings.json`) and the hooks fire in EVERY project on the
|
|
544
|
+
* machine — the root cause of the 2026-06-03 global-hook-hang incident, where
|
|
545
|
+
* audit-logger.sh stacked up and froze Claude Code across all terminals.
|
|
546
|
+
*
|
|
547
|
+
* Throws an actionable Error if `projectDir` is the home directory, or if
|
|
548
|
+
* `<projectDir>/.claude` is the global Claude config dir (honoring a relocated
|
|
549
|
+
* CLAUDE_CONFIG_DIR). The CLI handler converts the throw into a clean
|
|
550
|
+
* stderr message + exit 1; calling installHooks directly still fails closed.
|
|
551
|
+
*/
|
|
552
|
+
function assertSafeInstallDir(projectDir) {
|
|
553
|
+
// Resolve symlinks so a link pointing at $HOME can't slip past a string-only
|
|
554
|
+
// comparison (review S1). realpathSync throws ENOENT for a path that doesn't
|
|
555
|
+
// exist yet — fall back to lexical resolution then (a non-existent dir can't
|
|
556
|
+
// be a symlink to home).
|
|
557
|
+
const real = (p) => {
|
|
558
|
+
try {
|
|
559
|
+
return fs.realpathSync(p);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return path.resolve(p);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const resolved = real(projectDir);
|
|
566
|
+
const home = real(os.homedir());
|
|
567
|
+
if (resolved === home) {
|
|
568
|
+
throw new Error(`Refusing to install governance into your home directory (${home}).\n` +
|
|
569
|
+
`That would write hooks into your GLOBAL Claude Code config ` +
|
|
570
|
+
`(~/.claude/settings.json) and run them in every project on this machine.\n` +
|
|
571
|
+
`cd into a specific project (one with package.json or .git) and run ` +
|
|
572
|
+
`'ai-governance init' there.`);
|
|
573
|
+
}
|
|
574
|
+
const globalClaudeDir = real(process.env['CLAUDE_CONFIG_DIR'] || path.join(home, '.claude'));
|
|
575
|
+
if (path.join(resolved, '.claude') === globalClaudeDir) {
|
|
576
|
+
throw new Error(`Refusing to install: ${path.join(projectDir, '.claude')} is your global ` +
|
|
577
|
+
`Claude config directory. Governance must be installed into a project, not ` +
|
|
578
|
+
`globally, or its hooks run in every project on this machine.\n` +
|
|
579
|
+
`cd into a specific project and run 'ai-governance init' there.`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
511
582
|
function installHooks(projectDir, config) {
|
|
583
|
+
// Fail closed before any scaffolding: never poison the global Claude config.
|
|
584
|
+
assertSafeInstallDir(projectDir);
|
|
512
585
|
const errors = [];
|
|
513
586
|
let installed = 0;
|
|
514
587
|
// Find the package's hooks directory (bash or powershell based on OS)
|
|
@@ -585,13 +658,27 @@ function installClaudeCodeSettings(projectDir, config, hooksDestDir) {
|
|
|
585
658
|
fs.mkdirSync(claudeSettingsDir, { recursive: true });
|
|
586
659
|
}
|
|
587
660
|
const settingsFile = path.join(claudeSettingsDir, 'settings.json');
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
661
|
+
// Resolve a coreutils timeout binary on THIS machine (init runs on the
|
|
662
|
+
// customer's box, same OS as runtime). GNU `timeout` ships on Linux;
|
|
663
|
+
// `gtimeout` is the macOS+coreutils name; a default macOS has neither and
|
|
664
|
+
// Windows has none. When present we wrap the hook command as a hard
|
|
665
|
+
// self-limit; when absent we emit the plain command and rely solely on the
|
|
666
|
+
// host-enforced `timeout` field below — never a hard dependency on a binary
|
|
667
|
+
// that may be missing (which would make the hook fail to run).
|
|
668
|
+
const timeoutBin = resolveTimeoutBin();
|
|
669
|
+
const buildBlock = (hookFile) => {
|
|
670
|
+
const hookPath = path.join(hooksDestDir, hookFile);
|
|
671
|
+
// `-k 2` escalates to SIGKILL 2s after SIGTERM. Both layers honor
|
|
672
|
+
// Invariant 2: the host-enforced `timeout` field bounds the hook on every
|
|
673
|
+
// platform; the wrapper adds a process-level self-limit where available.
|
|
674
|
+
const command = timeoutBin
|
|
675
|
+
? `${timeoutBin} -k 2 ${HOOK_TIMEOUT_SECONDS} ${HOOK_RUNNER} "${hookPath}"`
|
|
676
|
+
: `${HOOK_RUNNER} "${hookPath}"`;
|
|
677
|
+
return {
|
|
678
|
+
matcher: '*',
|
|
679
|
+
hooks: [{ type: 'command', command, timeout: HOOK_TIMEOUT_SECONDS }],
|
|
680
|
+
};
|
|
681
|
+
};
|
|
595
682
|
const preEntries = config.hooks.preToolUse.map(buildBlock);
|
|
596
683
|
const postEntries = config.hooks.postToolUse.map(buildBlock);
|
|
597
684
|
let settings = {};
|
|
@@ -604,13 +691,119 @@ function installClaudeCodeSettings(projectDir, config, hooksDestDir) {
|
|
|
604
691
|
const existingHooks = (settings['hooks'] && typeof settings['hooks'] === 'object' && !Array.isArray(settings['hooks']))
|
|
605
692
|
? settings['hooks']
|
|
606
693
|
: {};
|
|
694
|
+
// Merge, never clobber. A previous version replaced the whole
|
|
695
|
+
// PreToolUse/PostToolUse arrays, which wiped any hooks the user added and
|
|
696
|
+
// made re-running `init` destructive. Keep the user's hooks, drop only our
|
|
697
|
+
// own previous entries (identified by a command referencing this project's
|
|
698
|
+
// hooks dir), then append the fresh set — idempotent, no duplicates.
|
|
699
|
+
// Match on the hooks dir WITH a trailing separator so a sibling like
|
|
700
|
+
// `.governance/hooks-extra/` is not mistaken for ours (review S2). Generated
|
|
701
|
+
// commands always reference `<hooksDestDir>/<file>`, so the separator is
|
|
702
|
+
// present for our entries.
|
|
703
|
+
const ourHooksPrefix = hooksDestDir + path.sep;
|
|
704
|
+
const isOurs = (block) => {
|
|
705
|
+
const inner = block?.hooks;
|
|
706
|
+
return Array.isArray(inner) && inner.some((h) => typeof h?.command === 'string' &&
|
|
707
|
+
(h.command).includes(ourHooksPrefix));
|
|
708
|
+
};
|
|
709
|
+
const mergeEvent = (existing, fresh) => [
|
|
710
|
+
...(Array.isArray(existing) ? existing.filter((b) => !isOurs(b)) : []),
|
|
711
|
+
...fresh,
|
|
712
|
+
];
|
|
607
713
|
settings['hooks'] = {
|
|
608
714
|
...existingHooks,
|
|
609
|
-
PreToolUse: preEntries,
|
|
610
|
-
PostToolUse: postEntries,
|
|
715
|
+
PreToolUse: mergeEvent(existingHooks['PreToolUse'], preEntries),
|
|
716
|
+
PostToolUse: mergeEvent(existingHooks['PostToolUse'], postEntries),
|
|
611
717
|
};
|
|
612
718
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
613
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Clean removal escape hatch for `ai-governance uninstall`.
|
|
722
|
+
*
|
|
723
|
+
* The 2026-06-03 incident had NO uninstall path, so recovery meant manually
|
|
724
|
+
* killing processes and `rm -rf`. This:
|
|
725
|
+
* 1. strips ONLY our hooks from `<projectDir>/.claude/settings.json`
|
|
726
|
+
* (identified by a command referencing `<projectDir>/.governance/hooks/`),
|
|
727
|
+
* preserving any hooks the user added themselves;
|
|
728
|
+
* 2. removes the generated hook scripts and `.governance.json`;
|
|
729
|
+
* 3. PRESERVES the tamper-evident audit chain (`.governance/audit/`) by
|
|
730
|
+
* default — it is compliance evidence — deleting it only when `purge` is
|
|
731
|
+
* set.
|
|
732
|
+
*
|
|
733
|
+
* Idempotent and non-throwing on a project with nothing installed.
|
|
734
|
+
*/
|
|
735
|
+
function uninstallGovernance(projectDir, opts = {}) {
|
|
736
|
+
const removedPaths = [];
|
|
737
|
+
let hooksRemoved = 0;
|
|
738
|
+
const govDir = path.join(projectDir, '.governance');
|
|
739
|
+
const govHooksDir = path.join(govDir, 'hooks');
|
|
740
|
+
// 1. Strip our hooks from .claude/settings.json, keeping the user's own.
|
|
741
|
+
const settingsFile = path.join(projectDir, '.claude', 'settings.json');
|
|
742
|
+
if (fs.existsSync(settingsFile)) {
|
|
743
|
+
try {
|
|
744
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
|
745
|
+
const hooks = settings['hooks'];
|
|
746
|
+
if (hooks && typeof hooks === 'object' && !Array.isArray(hooks)) {
|
|
747
|
+
const hookMap = hooks;
|
|
748
|
+
for (const event of ['PreToolUse', 'PostToolUse']) {
|
|
749
|
+
const arr = hookMap[event];
|
|
750
|
+
if (!Array.isArray(arr))
|
|
751
|
+
continue;
|
|
752
|
+
// Trailing separator so a sibling like `.governance/hooks-extra/` is
|
|
753
|
+
// not removed as ours (review S2). Our commands reference
|
|
754
|
+
// `<govHooksDir>/<file>`, so the separator is present for our entries.
|
|
755
|
+
const ourHooksPrefix = govHooksDir + path.sep;
|
|
756
|
+
hookMap[event] = arr.filter((block) => {
|
|
757
|
+
const inner = block?.hooks;
|
|
758
|
+
const isOurs = Array.isArray(inner) && inner.some((h) => typeof h?.command === 'string' &&
|
|
759
|
+
(h.command).includes(ourHooksPrefix));
|
|
760
|
+
if (isOurs)
|
|
761
|
+
hooksRemoved++;
|
|
762
|
+
return !isOurs;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
// Malformed settings.json: leave it untouched for the user to inspect.
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// 2. Remove the runtime config file (carries serviceToken etc.).
|
|
773
|
+
const cfg = path.join(projectDir, '.governance.json');
|
|
774
|
+
if (fs.existsSync(cfg)) {
|
|
775
|
+
fs.rmSync(cfg, { force: true });
|
|
776
|
+
removedPaths.push(cfg);
|
|
777
|
+
}
|
|
778
|
+
// 3. Remove generated artifacts; preserve the audit chain unless purging.
|
|
779
|
+
let auditPreserved = null;
|
|
780
|
+
if (fs.existsSync(govDir)) {
|
|
781
|
+
const auditDir = path.join(govDir, 'audit');
|
|
782
|
+
const hasAudit = !opts.purge && fs.existsSync(auditDir) &&
|
|
783
|
+
fs.readdirSync(auditDir).length > 0;
|
|
784
|
+
if (opts.purge) {
|
|
785
|
+
fs.rmSync(govDir, { recursive: true, force: true });
|
|
786
|
+
removedPaths.push(govDir);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
for (const entry of fs.readdirSync(govDir)) {
|
|
790
|
+
if (entry === 'audit' && hasAudit)
|
|
791
|
+
continue;
|
|
792
|
+
const p = path.join(govDir, entry);
|
|
793
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
794
|
+
removedPaths.push(p);
|
|
795
|
+
}
|
|
796
|
+
if (hasAudit) {
|
|
797
|
+
auditPreserved = auditDir;
|
|
798
|
+
}
|
|
799
|
+
else if (fs.readdirSync(govDir).length === 0) {
|
|
800
|
+
fs.rmSync(govDir, { recursive: true, force: true });
|
|
801
|
+
removedPaths.push(govDir);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return { hooksRemoved, removedPaths, auditPreserved };
|
|
806
|
+
}
|
|
614
807
|
const IDE_RULES_SPECS = {
|
|
615
808
|
cursor: {
|
|
616
809
|
ideKey: 'cursor',
|
|
@@ -2116,6 +2309,8 @@ Usage:
|
|
|
2116
2309
|
ai-governance verify-chain <logDir> Verify audit chain integrity (standalone)
|
|
2117
2310
|
[--json] Output machine-readable JSON
|
|
2118
2311
|
ai-governance status Show governance status
|
|
2312
|
+
ai-governance uninstall Cleanly remove governance from this project
|
|
2313
|
+
[--purge] Also delete the audit chain (default: preserved)
|
|
2119
2314
|
ai-governance discover Scan project + push signed manifests
|
|
2120
2315
|
--root <path> Project directory to scan
|
|
2121
2316
|
--org-id <org> Organisation identifier
|
|
@@ -2190,6 +2385,15 @@ Connexum Network Inc.
|
|
|
2190
2385
|
if (isDirectRun) {
|
|
2191
2386
|
const projectDir = process.cwd();
|
|
2192
2387
|
if (command === 'init') {
|
|
2388
|
+
// P0: refuse to scaffold into the home dir / global Claude config before
|
|
2389
|
+
// doing any provisioning. Friendly message instead of a deep stack trace.
|
|
2390
|
+
try {
|
|
2391
|
+
assertSafeInstallDir(projectDir);
|
|
2392
|
+
}
|
|
2393
|
+
catch (e) {
|
|
2394
|
+
process.stderr.write(`[CLI] ${e.message}\n`);
|
|
2395
|
+
process.exit(1);
|
|
2396
|
+
}
|
|
2193
2397
|
const ciMode = args.includes('--ci');
|
|
2194
2398
|
// S-07: Parse vendor code flags
|
|
2195
2399
|
const vendorCodeIdx = args.indexOf('--vendor-code');
|
|
@@ -2662,6 +2866,23 @@ if (isDirectRun) {
|
|
|
2662
2866
|
else if (command === 'status') {
|
|
2663
2867
|
showStatus(projectDir);
|
|
2664
2868
|
}
|
|
2869
|
+
else if (command === 'uninstall') {
|
|
2870
|
+
// Clean removal escape hatch. Strips our hooks from .claude/settings.json
|
|
2871
|
+
// (keeping the user's own), removes generated scripts + .governance.json,
|
|
2872
|
+
// and preserves the audit chain unless --purge.
|
|
2873
|
+
const purge = args.includes('--purge');
|
|
2874
|
+
const result = uninstallGovernance(projectDir, { purge });
|
|
2875
|
+
process.stdout.write(`[CLI] Removed ${result.hooksRemoved} governance hook(s) from .claude/settings.json\n`);
|
|
2876
|
+
for (const p of result.removedPaths) {
|
|
2877
|
+
process.stdout.write(`[CLI] Removed ${p}\n`);
|
|
2878
|
+
}
|
|
2879
|
+
if (result.auditPreserved) {
|
|
2880
|
+
process.stdout.write(`[CLI] Audit chain preserved at ${result.auditPreserved} ` +
|
|
2881
|
+
`(re-run with --purge to delete it too).\n`);
|
|
2882
|
+
}
|
|
2883
|
+
process.stdout.write('[CLI] Governance uninstalled. Restart Claude Code for the change to take effect.\n');
|
|
2884
|
+
process.exit(0);
|
|
2885
|
+
}
|
|
2665
2886
|
else if (command === 'packs') {
|
|
2666
2887
|
// W2: per-project pack selection CLI subcommand
|
|
2667
2888
|
Promise.resolve().then(() => __importStar(require('./packs.js'))).then(({ runPacksCommand }) => {
|