@open-product-primer/cli 2.10.0 → 2.11.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.
- package/dist/cli.js +2 -0
- package/dist/commands/claude-mods.d.ts +2 -0
- package/dist/commands/claude-mods.js +45 -0
- package/dist/commands/init.js +16 -0
- package/dist/lib/claude-mods.d.ts +15 -0
- package/dist/lib/claude-mods.js +78 -0
- package/dist/lib/config-merge.js +2 -0
- package/dist/lib/detect.d.ts +2 -0
- package/dist/lib/detect.js +20 -0
- package/dist/lib/install-agent.d.ts +12 -0
- package/dist/lib/install-agent.js +141 -0
- package/dist/lib/templates.js +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ const context_1 = require("./commands/context");
|
|
|
16
16
|
const list_1 = require("./commands/list");
|
|
17
17
|
const show_1 = require("./commands/show");
|
|
18
18
|
const status_1 = require("./commands/status");
|
|
19
|
+
const claude_mods_1 = require("./commands/claude-mods");
|
|
19
20
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
20
21
|
const program = new commander_1.Command();
|
|
21
22
|
program
|
|
@@ -33,4 +34,5 @@ program.addCommand((0, context_1.contextCommand)());
|
|
|
33
34
|
program.addCommand((0, list_1.listCommand)());
|
|
34
35
|
program.addCommand((0, show_1.showCommand)());
|
|
35
36
|
program.addCommand((0, status_1.statusCommand)());
|
|
37
|
+
program.addCommand((0, claude_mods_1.claudeModsCommand)());
|
|
36
38
|
program.parse();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.claudeModsCommand = claudeModsCommand;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const detect_1 = require("../lib/detect");
|
|
10
|
+
const install_agent_1 = require("../lib/install-agent");
|
|
11
|
+
function claudeModsCommand() {
|
|
12
|
+
return new commander_1.Command('claude-mods')
|
|
13
|
+
.description('Select which Claude Code function-hook mods are installed')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
const projectRoot = process.cwd();
|
|
16
|
+
const agents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
17
|
+
if (!agents || !agents.includes('claude')) {
|
|
18
|
+
console.error(chalk_1.default.red('Claude Code is not installed in this project.') +
|
|
19
|
+
' Run ' +
|
|
20
|
+
chalk_1.default.cyan('oprim init') +
|
|
21
|
+
' and select Claude Code first.');
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const previousMods = (0, detect_1.readClaudeModsFromConfig)(projectRoot);
|
|
26
|
+
const selectedMods = await (0, install_agent_1.promptClaudeModsSelection)(previousMods);
|
|
27
|
+
if (selectedMods.length > 0 && !(0, install_agent_1.isFunctionHooksActive)(projectRoot)) {
|
|
28
|
+
const enable = await (0, install_agent_1.promptEnableFunctionHooks)();
|
|
29
|
+
if (enable) {
|
|
30
|
+
(0, install_agent_1.enableFunctionHooks)(projectRoot);
|
|
31
|
+
console.log(chalk_1.default.green('✓') + ' .claude/settings.json (function hooks enabled)');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
(0, install_agent_1.printManualFunctionHooksActivation)();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
(0, install_agent_1.applyClaudeModsSelection)(projectRoot, selectedMods, previousMods);
|
|
38
|
+
if (selectedMods.length === 0) {
|
|
39
|
+
console.log(chalk_1.default.dim(' No mods selected — any previously-installed mod hooks were removed.'));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(chalk_1.default.green('✓') + ` Claude mods installed: ${selectedMods.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -147,6 +147,22 @@ function initCommand() {
|
|
|
147
147
|
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
|
|
148
148
|
}
|
|
149
149
|
console.log('\n' + chalk_1.default.green('✓') + ` Agent skills installed: ${selectedAgents.join(', ')}`);
|
|
150
|
+
if (selectedAgents.includes('claude')) {
|
|
151
|
+
const previousMods = (0, detect_1.readClaudeModsFromConfig)(projectRoot);
|
|
152
|
+
console.log('');
|
|
153
|
+
const selectedMods = await (0, install_agent_1.promptClaudeModsSelection)(previousMods);
|
|
154
|
+
if (selectedMods.length > 0 && !(0, install_agent_1.isFunctionHooksActive)(projectRoot)) {
|
|
155
|
+
const enable = await (0, install_agent_1.promptEnableFunctionHooks)();
|
|
156
|
+
if (enable) {
|
|
157
|
+
(0, install_agent_1.enableFunctionHooks)(projectRoot);
|
|
158
|
+
console.log(chalk_1.default.green('✓') + ' .claude/settings.json (function hooks enabled)');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
(0, install_agent_1.printManualFunctionHooksActivation)();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
(0, install_agent_1.applyClaudeModsSelection)(projectRoot, selectedMods, previousMods);
|
|
165
|
+
}
|
|
150
166
|
}
|
|
151
167
|
console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
|
|
152
168
|
console.log('\n' +
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ClaudeModHookFile {
|
|
2
|
+
filename: string;
|
|
3
|
+
content: string;
|
|
4
|
+
event: 'PreToolUse' | 'PostToolUse' | 'UserPromptSubmit' | 'Stop';
|
|
5
|
+
matcher?: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ClaudeMod {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
hookFiles: ClaudeModHookFile[];
|
|
13
|
+
}
|
|
14
|
+
export declare const CLAUDE_MODS_REGISTRY: ClaudeMod[];
|
|
15
|
+
export declare function getClaudeMod(id: string): ClaudeMod | undefined;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Static registry of installable Claude Code function-hook "mods" — see
|
|
3
|
+
// oprim/bets/pending/BET-051-spec-delta-drift-interceptor-mod/design.md. Both `oprim init`'s
|
|
4
|
+
// mod-selection prompt and the `oprim claude-mods` command read from this single registry, so a
|
|
5
|
+
// future mod (BET-052–066) is added here once rather than duplicated per entry point.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CLAUDE_MODS_REGISTRY = void 0;
|
|
8
|
+
exports.getClaudeMod = getClaudeMod;
|
|
9
|
+
// PostToolUse hook: on a Write/Edit to a bet's specs/<capability>/spec.md delta, shells out to
|
|
10
|
+
// `oprim validate --json` (already runs checkSpecDeltaDrift()) and surfaces any MODIFIED/REMOVED
|
|
11
|
+
// drift for that capability inline, at write time instead of only at validate/CI time. Silently
|
|
12
|
+
// no-ops on any failure (oprim not resolvable, malformed hook input, etc.) — this is a
|
|
13
|
+
// write-time convenience on top of `oprim validate`, never a second source of required failures.
|
|
14
|
+
const SPEC_DELTA_DRIFT_INTERCEPTOR_SCRIPT = `#!/usr/bin/env node
|
|
15
|
+
// PostToolUse hook (Write|Edit) — see claude-mods.ts's spec-delta-drift-interceptor entry.
|
|
16
|
+
|
|
17
|
+
let raw = '';
|
|
18
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
19
|
+
process.stdin.on('end', () => {
|
|
20
|
+
try {
|
|
21
|
+
const input = JSON.parse(raw || '{}');
|
|
22
|
+
const filePath = input.tool_input && input.tool_input.file_path;
|
|
23
|
+
if (!filePath) return;
|
|
24
|
+
|
|
25
|
+
const match = String(filePath).replace(/\\\\/g, '/').match(
|
|
26
|
+
/oprim\\/bets\\/pending\\/[^/]+\\/specs\\/([^/]+)\\/spec\\.md$/
|
|
27
|
+
);
|
|
28
|
+
if (!match) return;
|
|
29
|
+
const capability = match[1];
|
|
30
|
+
|
|
31
|
+
const { execSync } = require('child_process');
|
|
32
|
+
let output;
|
|
33
|
+
try {
|
|
34
|
+
output = execSync('npx --no-install oprim validate --json', {
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// validate exits non-zero when a required check fails — stdout still carries the report
|
|
40
|
+
output = err && err.stdout ? err.stdout.toString() : null;
|
|
41
|
+
}
|
|
42
|
+
if (!output) return;
|
|
43
|
+
|
|
44
|
+
const report = JSON.parse(output);
|
|
45
|
+
const drift = (report.checks || []).filter(
|
|
46
|
+
(c) => c.name && c.name.startsWith('spec-delta:') && c.name.includes(\`in \${capability} \`) && !c.pass
|
|
47
|
+
);
|
|
48
|
+
if (drift.length === 0) return;
|
|
49
|
+
|
|
50
|
+
const reason = drift.map((c) => \`- \${c.name}\${c.note ? \` (\${c.note})\` : ''}\`).join('\\n');
|
|
51
|
+
process.stdout.write(JSON.stringify({
|
|
52
|
+
decision: 'block',
|
|
53
|
+
reason: \`Spec-delta drift detected in \${capability}:\\n\${reason}\`,
|
|
54
|
+
}) + '\\n');
|
|
55
|
+
} catch {
|
|
56
|
+
// Graceful degradation — never error or block on interceptor failure.
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
`;
|
|
60
|
+
exports.CLAUDE_MODS_REGISTRY = [
|
|
61
|
+
{
|
|
62
|
+
id: 'spec-delta-drift-interceptor',
|
|
63
|
+
title: 'Spec-delta drift interceptor',
|
|
64
|
+
description: "Catches a bet's spec-delta MODIFIED/REMOVED requirement drifting from oprim/specs current truth at write time, instead of only at oprim validate/CI time.",
|
|
65
|
+
hookFiles: [
|
|
66
|
+
{
|
|
67
|
+
filename: 'spec-delta-drift-interceptor.js',
|
|
68
|
+
content: SPEC_DELTA_DRIFT_INTERCEPTOR_SCRIPT,
|
|
69
|
+
event: 'PostToolUse',
|
|
70
|
+
matcher: 'Write|Edit',
|
|
71
|
+
command: 'node ".claude/hooks/spec-delta-drift-interceptor.js"',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
function getClaudeMod(id) {
|
|
77
|
+
return exports.CLAUDE_MODS_REGISTRY.find((mod) => mod.id === id);
|
|
78
|
+
}
|
package/dist/lib/config-merge.js
CHANGED
|
@@ -12,6 +12,8 @@ const CONFIG_SCHEMA_FIELDS = [
|
|
|
12
12
|
// merge only adds missing keys, never removes existing ones), but new/updated configs only
|
|
13
13
|
// ever gain `remote_context`.
|
|
14
14
|
{ key: 'remote_context', block: 'remote_context:\n enabled: false\n sources: []\n' },
|
|
15
|
+
// bet-051 — installed claude-mods selection, mirroring how `agents:` is stored/rewritten.
|
|
16
|
+
{ key: 'claude_mods', block: 'claude_mods: []\n' },
|
|
15
17
|
];
|
|
16
18
|
function existingTopLevelKeys(content) {
|
|
17
19
|
const keys = new Set();
|
package/dist/lib/detect.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export declare function readAgentsFromConfig(projectRoot: string): string[] | nu
|
|
|
10
10
|
export declare function readOkfEnabledFromConfig(projectRoot: string): boolean;
|
|
11
11
|
export declare function detectAvailableAgents(projectRoot: string): string[];
|
|
12
12
|
export declare function writeAgentsToConfig(agents: string[], projectRoot: string): void;
|
|
13
|
+
export declare function readClaudeModsFromConfig(projectRoot: string): string[];
|
|
14
|
+
export declare function writeClaudeModsToConfig(mods: string[], projectRoot: string): void;
|
package/dist/lib/detect.js
CHANGED
|
@@ -39,6 +39,8 @@ exports.readAgentsFromConfig = readAgentsFromConfig;
|
|
|
39
39
|
exports.readOkfEnabledFromConfig = readOkfEnabledFromConfig;
|
|
40
40
|
exports.detectAvailableAgents = detectAvailableAgents;
|
|
41
41
|
exports.writeAgentsToConfig = writeAgentsToConfig;
|
|
42
|
+
exports.readClaudeModsFromConfig = readClaudeModsFromConfig;
|
|
43
|
+
exports.writeClaudeModsToConfig = writeClaudeModsToConfig;
|
|
42
44
|
const fs = __importStar(require("fs"));
|
|
43
45
|
const path = __importStar(require("path"));
|
|
44
46
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -103,3 +105,21 @@ function writeAgentsToConfig(agents, projectRoot) {
|
|
|
103
105
|
config['agents'] = agents;
|
|
104
106
|
fs.writeFileSync(configPath, yaml.dump(config, { indent: 2 }), 'utf-8');
|
|
105
107
|
}
|
|
108
|
+
function readClaudeModsFromConfig(projectRoot) {
|
|
109
|
+
const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
110
|
+
if (!fs.existsSync(configPath))
|
|
111
|
+
return [];
|
|
112
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
113
|
+
const config = yaml.load(content);
|
|
114
|
+
const mods = config?.['claude_mods'];
|
|
115
|
+
return Array.isArray(mods) ? mods : [];
|
|
116
|
+
}
|
|
117
|
+
function writeClaudeModsToConfig(mods, projectRoot) {
|
|
118
|
+
const configPath = path.join(projectRoot, 'oprim', 'config.yaml');
|
|
119
|
+
if (!fs.existsSync(configPath))
|
|
120
|
+
return;
|
|
121
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
122
|
+
const config = yaml.load(content);
|
|
123
|
+
config['claude_mods'] = mods;
|
|
124
|
+
fs.writeFileSync(configPath, yaml.dump(config, { indent: 2 }), 'utf-8');
|
|
125
|
+
}
|
|
@@ -3,6 +3,18 @@ export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
4
|
export declare function resolveSpecFramework(projectRoot: string): string;
|
|
5
5
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
6
|
+
export declare function promptClaudeModsSelection(preChecked: string[]): Promise<string[]>;
|
|
7
|
+
export declare function isFunctionHooksActive(projectRoot: string): boolean;
|
|
8
|
+
export declare function promptEnableFunctionHooks(): Promise<boolean>;
|
|
9
|
+
export declare function enableFunctionHooks(projectRoot: string): void;
|
|
10
|
+
export declare function printManualFunctionHooksActivation(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Merges newly-selected mods' hookFiles into .claude/settings.json (additive, non-clobbering —
|
|
13
|
+
* same convention as mergeClaudeSettingsHooks) and removes deselected mods' entries, writing/
|
|
14
|
+
* deleting each mod's hook script file(s) to match. Persists the resulting selection to
|
|
15
|
+
* oprim/config.yaml's claude_mods key.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyClaudeModsSelection(projectRoot: string, selectedIds: string[], previousIds: string[]): void;
|
|
6
18
|
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
7
19
|
export declare function promptOkfFrontmatter(): Promise<boolean>;
|
|
8
20
|
export declare function installAgentSkills(agent: Agent, projectRoot: string, framework?: string, pdrSurfacing?: boolean): void;
|
|
@@ -40,6 +40,12 @@ exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.DSH_SKILLS = exports.K
|
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
41
|
exports.resolveSpecFramework = resolveSpecFramework;
|
|
42
42
|
exports.promptAgentSelection = promptAgentSelection;
|
|
43
|
+
exports.promptClaudeModsSelection = promptClaudeModsSelection;
|
|
44
|
+
exports.isFunctionHooksActive = isFunctionHooksActive;
|
|
45
|
+
exports.promptEnableFunctionHooks = promptEnableFunctionHooks;
|
|
46
|
+
exports.enableFunctionHooks = enableFunctionHooks;
|
|
47
|
+
exports.printManualFunctionHooksActivation = printManualFunctionHooksActivation;
|
|
48
|
+
exports.applyClaudeModsSelection = applyClaudeModsSelection;
|
|
43
49
|
exports.promptPdrSurfacing = promptPdrSurfacing;
|
|
44
50
|
exports.promptOkfFrontmatter = promptOkfFrontmatter;
|
|
45
51
|
exports.installAgentSkills = installAgentSkills;
|
|
@@ -60,6 +66,7 @@ const detect_1 = require("./detect");
|
|
|
60
66
|
const config_merge_1 = require("./config-merge");
|
|
61
67
|
const workflow_schema_1 = require("./workflow-schema");
|
|
62
68
|
const workflow_renderer_1 = require("./workflow-renderer");
|
|
69
|
+
const claude_mods_1 = require("./claude-mods");
|
|
63
70
|
exports.SUPPORTED_AGENTS = [
|
|
64
71
|
'claude',
|
|
65
72
|
'cursor',
|
|
@@ -145,6 +152,140 @@ async function promptAgentSelection(projectRoot) {
|
|
|
145
152
|
],
|
|
146
153
|
});
|
|
147
154
|
}
|
|
155
|
+
// ─── claude-mods ───────────────────────────────────────────────────────────
|
|
156
|
+
async function promptClaudeModsSelection(preChecked) {
|
|
157
|
+
const { checkbox } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
158
|
+
return checkbox({
|
|
159
|
+
message: 'Which Claude Code mods should be installed?',
|
|
160
|
+
choices: claude_mods_1.CLAUDE_MODS_REGISTRY.map((mod) => ({
|
|
161
|
+
name: `${mod.title} — ${mod.description}`,
|
|
162
|
+
value: mod.id,
|
|
163
|
+
checked: preChecked.includes(mod.id),
|
|
164
|
+
})),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function isFunctionHooksActive(projectRoot) {
|
|
168
|
+
if (process.env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS)
|
|
169
|
+
return true;
|
|
170
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
171
|
+
if (!fs.existsSync(settingsPath))
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
175
|
+
const env = settings.env;
|
|
176
|
+
return Boolean(env?.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function promptEnableFunctionHooks() {
|
|
183
|
+
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
184
|
+
return confirm({ message: 'Enable Claude Code function hooks now? (y/n)', default: true });
|
|
185
|
+
}
|
|
186
|
+
function enableFunctionHooks(projectRoot) {
|
|
187
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
188
|
+
const settings = readSettings(settingsPath);
|
|
189
|
+
if (!settings.env)
|
|
190
|
+
settings.env = {};
|
|
191
|
+
settings.env.CLAUDE_CODE_ENABLE_FUNCTION_HOOKS = '1';
|
|
192
|
+
writeSettings(settingsPath, settings);
|
|
193
|
+
}
|
|
194
|
+
function printManualFunctionHooksActivation() {
|
|
195
|
+
console.log(chalk_1.default.yellow(' Function hooks are not active.') +
|
|
196
|
+
' Selected mod(s) will no-op until you either:\n' +
|
|
197
|
+
' - set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in your shell environment, or\n' +
|
|
198
|
+
' - add an "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } block to .claude/settings.json');
|
|
199
|
+
}
|
|
200
|
+
function readSettings(settingsPath) {
|
|
201
|
+
if (!fs.existsSync(settingsPath))
|
|
202
|
+
return {};
|
|
203
|
+
try {
|
|
204
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function writeSettings(settingsPath, settings) {
|
|
211
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
function addModHookToSettings(settings, hookFile) {
|
|
214
|
+
if (!settings.hooks)
|
|
215
|
+
settings.hooks = {};
|
|
216
|
+
const hooks = settings.hooks;
|
|
217
|
+
if (!hooks[hookFile.event])
|
|
218
|
+
hooks[hookFile.event] = [];
|
|
219
|
+
const entries = hooks[hookFile.event];
|
|
220
|
+
const present = entries.some((entry) => {
|
|
221
|
+
const entryHooks = entry.hooks;
|
|
222
|
+
return entryHooks?.some((h) => h.command === hookFile.command);
|
|
223
|
+
});
|
|
224
|
+
if (!present) {
|
|
225
|
+
const entry = { hooks: [{ type: 'command', command: hookFile.command }] };
|
|
226
|
+
if (hookFile.matcher)
|
|
227
|
+
entry.matcher = hookFile.matcher;
|
|
228
|
+
entries.push(entry);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function removeModHookFromSettings(settings, hookFile) {
|
|
232
|
+
const hooks = settings.hooks;
|
|
233
|
+
if (!hooks || !hooks[hookFile.event])
|
|
234
|
+
return;
|
|
235
|
+
const entries = hooks[hookFile.event];
|
|
236
|
+
const filtered = entries.filter((entry) => {
|
|
237
|
+
const entryHooks = entry.hooks;
|
|
238
|
+
return !entryHooks?.some((h) => h.command === hookFile.command);
|
|
239
|
+
});
|
|
240
|
+
if (filtered.length === 0) {
|
|
241
|
+
delete hooks[hookFile.event];
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
hooks[hookFile.event] = filtered;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Merges newly-selected mods' hookFiles into .claude/settings.json (additive, non-clobbering —
|
|
249
|
+
* same convention as mergeClaudeSettingsHooks) and removes deselected mods' entries, writing/
|
|
250
|
+
* deleting each mod's hook script file(s) to match. Persists the resulting selection to
|
|
251
|
+
* oprim/config.yaml's claude_mods key.
|
|
252
|
+
*/
|
|
253
|
+
function applyClaudeModsSelection(projectRoot, selectedIds, previousIds) {
|
|
254
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
255
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
256
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
257
|
+
const settings = readSettings(settingsPath);
|
|
258
|
+
const added = selectedIds.filter((id) => !previousIds.includes(id));
|
|
259
|
+
const removed = previousIds.filter((id) => !selectedIds.includes(id));
|
|
260
|
+
for (const id of added) {
|
|
261
|
+
const mod = (0, claude_mods_1.getClaudeMod)(id);
|
|
262
|
+
if (!mod)
|
|
263
|
+
continue;
|
|
264
|
+
for (const hookFile of mod.hookFiles) {
|
|
265
|
+
const scriptPath = path.join(hooksDir, hookFile.filename);
|
|
266
|
+
(0, scaffold_1.writeFile)(scriptPath, hookFile.content);
|
|
267
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
268
|
+
addModHookToSettings(settings, hookFile);
|
|
269
|
+
console.log(chalk_1.default.green('✓') + ` .claude/hooks/${hookFile.filename} (${mod.title})`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const id of removed) {
|
|
273
|
+
const mod = (0, claude_mods_1.getClaudeMod)(id);
|
|
274
|
+
if (!mod)
|
|
275
|
+
continue;
|
|
276
|
+
for (const hookFile of mod.hookFiles) {
|
|
277
|
+
const scriptPath = path.join(hooksDir, hookFile.filename);
|
|
278
|
+
if (fs.existsSync(scriptPath))
|
|
279
|
+
fs.unlinkSync(scriptPath);
|
|
280
|
+
removeModHookFromSettings(settings, hookFile);
|
|
281
|
+
console.log(chalk_1.default.dim(` removed .claude/hooks/${hookFile.filename} (${mod.title})`));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (added.length > 0 || removed.length > 0) {
|
|
285
|
+
writeSettings(settingsPath, settings);
|
|
286
|
+
}
|
|
287
|
+
(0, detect_1.writeClaudeModsToConfig)(selectedIds, projectRoot);
|
|
288
|
+
}
|
|
148
289
|
async function promptPdrSurfacing() {
|
|
149
290
|
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
150
291
|
return confirm({ message: 'Enable proactive PDR surfacing in skills? (Y/n)', default: true });
|
package/dist/lib/templates.js
CHANGED