@open-product-primer/cli 2.10.0 → 2.12.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 +29 -0
- package/dist/lib/claude-mods.js +103 -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 +14 -0
- package/dist/lib/install-agent.js +162 -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,29 @@
|
|
|
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 ClaudeModPluginFile {
|
|
9
|
+
path: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
interface ClaudeModBase {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ClaudeModClassic extends ClaudeModBase {
|
|
18
|
+
shape: 'classic';
|
|
19
|
+
hookFiles: ClaudeModHookFile[];
|
|
20
|
+
}
|
|
21
|
+
export interface ClaudeModPlugin extends ClaudeModBase {
|
|
22
|
+
shape: 'plugin';
|
|
23
|
+
pluginFiles: ClaudeModPluginFile[];
|
|
24
|
+
}
|
|
25
|
+
export type ClaudeMod = ClaudeModClassic | ClaudeModPlugin;
|
|
26
|
+
export declare function isPluginMod(mod: ClaudeMod): mod is ClaudeModPlugin;
|
|
27
|
+
export declare const CLAUDE_MODS_REGISTRY: ClaudeMod[];
|
|
28
|
+
export declare function getClaudeMod(id: string): ClaudeMod | undefined;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Static registry of installable Claude Code "mods" — see
|
|
3
|
+
// oprim/bets/pending/BET-074-migrate-claude-mods-to-function-hooks/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–073) is added here once rather than duplicated per entry point.
|
|
6
|
+
//
|
|
7
|
+
// A registry entry declares one of two shapes:
|
|
8
|
+
// - 'classic': a shell-command hook merged into .claude/settings.json's `hooks` block (text-only
|
|
9
|
+
// {decision, reason} protocol — cannot draw UI).
|
|
10
|
+
// - 'plugin': a Claude Code function-hooks plugin (manifest + hooks.json + a register(on) module)
|
|
11
|
+
// installed at .claude/skills/<id>/, loaded via the project-scope skills-directory auto-load
|
|
12
|
+
// convention. Can draw UI ($.ui.toast, $.ui.render) but requires
|
|
13
|
+
// CLAUDE_CODE_ENABLE_FUNCTION_HOOKS and is early access — its API may move between releases.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.CLAUDE_MODS_REGISTRY = void 0;
|
|
16
|
+
exports.isPluginMod = isPluginMod;
|
|
17
|
+
exports.getClaudeMod = getClaudeMod;
|
|
18
|
+
function isPluginMod(mod) {
|
|
19
|
+
return mod.shape === 'plugin';
|
|
20
|
+
}
|
|
21
|
+
// register(on) module: on a Write/Edit tool.call to a bet's specs/<capability>/spec.md delta,
|
|
22
|
+
// shells out to `oprim validate --json` (already runs checkSpecDeltaDrift()) and surfaces any
|
|
23
|
+
// MODIFIED/REMOVED drift for that capability via a toast, at write time instead of only at
|
|
24
|
+
// validate/CI time. Never denies the call — this is a write-time convenience on top of
|
|
25
|
+
// `oprim validate`, never a second source of required failures. Silently no-ops on any failure
|
|
26
|
+
// (oprim not resolvable, malformed report, etc.).
|
|
27
|
+
const SPEC_DELTA_DRIFT_INTERCEPTOR_REGISTER = `// Function-hooks module for the spec-delta-drift-interceptor mod — see claude-mods.ts.
|
|
28
|
+
// EARLY ACCESS: register(on)/$.ui.toast are gated behind CLAUDE_CODE_ENABLE_FUNCTION_HOOKS and
|
|
29
|
+
// may change shape between Claude Code releases.
|
|
30
|
+
|
|
31
|
+
import { execSync } from 'node:child_process';
|
|
32
|
+
|
|
33
|
+
export function register(on) {
|
|
34
|
+
on('tool.call', async ($, e, next) => {
|
|
35
|
+
const result = await next(e);
|
|
36
|
+
if (e.tool !== 'Write' && e.tool !== 'Edit') return result;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const filePath = String(e.file_path || '').replace(/\\\\/g, '/');
|
|
40
|
+
const match = filePath.match(/oprim\\/bets\\/pending\\/[^/]+\\/specs\\/([^/]+)\\/spec\\.md$/);
|
|
41
|
+
if (!match) return result;
|
|
42
|
+
const capability = match[1];
|
|
43
|
+
|
|
44
|
+
let output;
|
|
45
|
+
try {
|
|
46
|
+
output = execSync('npx --no-install oprim validate --json', {
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
49
|
+
});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// validate exits non-zero when a required check fails — stdout still carries the report
|
|
52
|
+
output = err && err.stdout ? err.stdout.toString() : null;
|
|
53
|
+
}
|
|
54
|
+
if (!output) return result;
|
|
55
|
+
|
|
56
|
+
const report = JSON.parse(output);
|
|
57
|
+
const drift = (report.checks || []).filter(
|
|
58
|
+
(c) => c.name && c.name.startsWith('spec-delta:') && c.name.includes(\`in \${capability} \`) && !c.pass
|
|
59
|
+
);
|
|
60
|
+
if (drift.length === 0) return result;
|
|
61
|
+
|
|
62
|
+
const reason = drift.map((c) => \`\${c.name}\${c.note ? \` (\${c.note})\` : ''}\`).join('; ');
|
|
63
|
+
$.ui.toast(\`Spec-delta drift in \${capability}: \${reason}\`, { timeoutMs: 8000 });
|
|
64
|
+
} catch {
|
|
65
|
+
// Graceful degradation — never error or block on interceptor failure.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
exports.CLAUDE_MODS_REGISTRY = [
|
|
73
|
+
{
|
|
74
|
+
id: 'spec-delta-drift-interceptor',
|
|
75
|
+
title: 'Spec-delta drift interceptor',
|
|
76
|
+
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.",
|
|
77
|
+
shape: 'plugin',
|
|
78
|
+
pluginFiles: [
|
|
79
|
+
{
|
|
80
|
+
path: '.claude-plugin/plugin.json',
|
|
81
|
+
content: JSON.stringify({
|
|
82
|
+
name: 'spec-delta-drift-interceptor',
|
|
83
|
+
description: "Catches a bet's spec-delta MODIFIED/REMOVED requirement drifting from oprim/specs current truth at write time.",
|
|
84
|
+
version: '1.0.0',
|
|
85
|
+
}, null, 2) + '\n',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: 'hooks/hooks.json',
|
|
89
|
+
content: JSON.stringify({
|
|
90
|
+
description: 'Runs checkSpecDeltaDrift() on a bet spec-delta write and surfaces drift via a toast, at write time.',
|
|
91
|
+
modules: ['./register.js'],
|
|
92
|
+
}, null, 2) + '\n',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
path: 'hooks/register.js',
|
|
96
|
+
content: SPEC_DELTA_DRIFT_INTERCEPTOR_REGISTER,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
function getClaudeMod(id) {
|
|
102
|
+
return exports.CLAUDE_MODS_REGISTRY.find((mod) => mod.id === id);
|
|
103
|
+
}
|
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,20 @@ 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 into the project and removes deselected mods' entries, writing/
|
|
13
|
+
* deleting each mod's files to match. A classic mod's hookFiles merge into .claude/settings.json
|
|
14
|
+
* (additive, non-clobbering — same convention as mergeClaudeSettingsHooks); a plugin mod's
|
|
15
|
+
* pluginFiles are written under .claude/skills/<mod.id>/ instead, where the skills-directory
|
|
16
|
+
* auto-load convention picks it up as a function-hooks plugin — settings.json is untouched for
|
|
17
|
+
* these. Persists the resulting selection to oprim/config.yaml's claude_mods key.
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyClaudeModsSelection(projectRoot: string, selectedIds: string[], previousIds: string[]): void;
|
|
6
20
|
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
7
21
|
export declare function promptOkfFrontmatter(): Promise<boolean>;
|
|
8
22
|
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,161 @@ 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 into the project and removes deselected mods' entries, writing/
|
|
249
|
+
* deleting each mod's files to match. A classic mod's hookFiles merge into .claude/settings.json
|
|
250
|
+
* (additive, non-clobbering — same convention as mergeClaudeSettingsHooks); a plugin mod's
|
|
251
|
+
* pluginFiles are written under .claude/skills/<mod.id>/ instead, where the skills-directory
|
|
252
|
+
* auto-load convention picks it up as a function-hooks plugin — settings.json is untouched for
|
|
253
|
+
* these. Persists the resulting selection to oprim/config.yaml's claude_mods key.
|
|
254
|
+
*/
|
|
255
|
+
function applyClaudeModsSelection(projectRoot, selectedIds, previousIds) {
|
|
256
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
257
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
258
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
259
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
260
|
+
const settings = readSettings(settingsPath);
|
|
261
|
+
let settingsChanged = false;
|
|
262
|
+
const added = selectedIds.filter((id) => !previousIds.includes(id));
|
|
263
|
+
const removed = previousIds.filter((id) => !selectedIds.includes(id));
|
|
264
|
+
for (const id of added) {
|
|
265
|
+
const mod = (0, claude_mods_1.getClaudeMod)(id);
|
|
266
|
+
if (!mod)
|
|
267
|
+
continue;
|
|
268
|
+
if ((0, claude_mods_1.isPluginMod)(mod)) {
|
|
269
|
+
const pluginDir = path.join(skillsDir, mod.id);
|
|
270
|
+
for (const file of mod.pluginFiles) {
|
|
271
|
+
(0, scaffold_1.writeFile)(path.join(pluginDir, file.path), file.content);
|
|
272
|
+
}
|
|
273
|
+
console.log(chalk_1.default.green('✓') + ` .claude/skills/${mod.id}/ (${mod.title})`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
for (const hookFile of mod.hookFiles) {
|
|
277
|
+
const scriptPath = path.join(hooksDir, hookFile.filename);
|
|
278
|
+
(0, scaffold_1.writeFile)(scriptPath, hookFile.content);
|
|
279
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
280
|
+
addModHookToSettings(settings, hookFile);
|
|
281
|
+
settingsChanged = true;
|
|
282
|
+
console.log(chalk_1.default.green('✓') + ` .claude/hooks/${hookFile.filename} (${mod.title})`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const id of removed) {
|
|
286
|
+
const mod = (0, claude_mods_1.getClaudeMod)(id);
|
|
287
|
+
if (!mod)
|
|
288
|
+
continue;
|
|
289
|
+
if ((0, claude_mods_1.isPluginMod)(mod)) {
|
|
290
|
+
const pluginDir = path.join(skillsDir, mod.id);
|
|
291
|
+
if (fs.existsSync(pluginDir))
|
|
292
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
293
|
+
console.log(chalk_1.default.dim(` removed .claude/skills/${mod.id}/ (${mod.title})`));
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
for (const hookFile of mod.hookFiles) {
|
|
297
|
+
const scriptPath = path.join(hooksDir, hookFile.filename);
|
|
298
|
+
if (fs.existsSync(scriptPath))
|
|
299
|
+
fs.unlinkSync(scriptPath);
|
|
300
|
+
removeModHookFromSettings(settings, hookFile);
|
|
301
|
+
settingsChanged = true;
|
|
302
|
+
console.log(chalk_1.default.dim(` removed .claude/hooks/${hookFile.filename} (${mod.title})`));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (settingsChanged) {
|
|
306
|
+
writeSettings(settingsPath, settings);
|
|
307
|
+
}
|
|
308
|
+
(0, detect_1.writeClaudeModsToConfig)(selectedIds, projectRoot);
|
|
309
|
+
}
|
|
148
310
|
async function promptPdrSurfacing() {
|
|
149
311
|
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
150
312
|
return confirm({ message: 'Enable proactive PDR surfacing in skills? (Y/n)', default: true });
|
package/dist/lib/templates.js
CHANGED