@codihaus/claude-skills 1.5.1 ā 1.6.1
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/package.json +1 -1
- package/skills/debrief/SKILL.md +10 -7
- package/skills/debrief/scripts/generate_questionnaire.py +0 -0
- package/skills/dev-coding/SKILL.md +302 -22
- package/skills/dev-scout/SKILL.md +36 -1
- package/skills/dev-specs/SKILL.md +25 -6
- package/src/commands/init.js +86 -14
- package/src/utils/config.js +54 -3
- package/src/utils/deps.js +164 -9
- package/src/utils/skills.js +27 -0
- package/templates/HOOK_TRIGGER_TEST.md +143 -0
- package/templates/hooks-guide.md +372 -0
- package/templates/scripts/safe-graph-update.sh +65 -0
package/src/commands/init.js
CHANGED
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
checkProjectDeps,
|
|
16
16
|
printDepsReport,
|
|
17
17
|
installProjectDeps,
|
|
18
|
-
|
|
18
|
+
installProjectDepsWithPM,
|
|
19
|
+
installPythonDeps,
|
|
20
|
+
installOptionalTools
|
|
19
21
|
} from '../utils/deps.js';
|
|
20
22
|
import {
|
|
21
23
|
getAvailableSkills,
|
|
@@ -33,11 +35,13 @@ import {
|
|
|
33
35
|
import {
|
|
34
36
|
runProjectSetup,
|
|
35
37
|
detectEnvExample,
|
|
36
|
-
hasEnvFile
|
|
38
|
+
hasEnvFile,
|
|
39
|
+
detectPackageManager
|
|
37
40
|
} from '../utils/project-setup.js';
|
|
38
41
|
|
|
39
42
|
export async function init(options) {
|
|
40
43
|
const projectPath = process.cwd();
|
|
44
|
+
let globalDeps = null; // Declare at function level for later use
|
|
41
45
|
|
|
42
46
|
console.log(chalk.bold('\nš Claude Skills Initialization\n'));
|
|
43
47
|
console.log(chalk.gray(`Project: ${projectPath}\n`));
|
|
@@ -45,7 +49,7 @@ export async function init(options) {
|
|
|
45
49
|
// Step 1: Check global dependencies
|
|
46
50
|
if (!options.noDeps) {
|
|
47
51
|
const spinner = ora('Checking system dependencies...').start();
|
|
48
|
-
|
|
52
|
+
globalDeps = await checkGlobalDeps();
|
|
49
53
|
spinner.stop();
|
|
50
54
|
|
|
51
55
|
printDepsReport(globalDeps);
|
|
@@ -69,19 +73,26 @@ export async function init(options) {
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
//
|
|
76
|
+
// Check if Python is available before offering to install packages
|
|
77
|
+
const hasPython = globalDeps.recommended.find(d => d.name === 'python3')?.found;
|
|
78
|
+
const hasPip = globalDeps.recommended.find(d => d.name === 'pip3')?.found;
|
|
79
|
+
|
|
80
|
+
// Offer to install missing Python packages (only if Python is installed)
|
|
73
81
|
const missingPython = globalDeps.python?.filter(p => !p.installed) || [];
|
|
74
|
-
if (missingPython.length > 0 && !options.yes) {
|
|
82
|
+
if (hasPython && hasPip && missingPython.length > 0 && !options.yes) {
|
|
75
83
|
const { installPython } = await inquirer.prompt([{
|
|
76
84
|
type: 'confirm',
|
|
77
85
|
name: 'installPython',
|
|
78
|
-
message: `Install missing Python packages (${missingPython.map(p => p.name).join(', ')})
|
|
86
|
+
message: `Install missing Python packages (${missingPython.map(p => p.name).join(', ')})? (will try --user first, then sudo if needed)`,
|
|
79
87
|
default: true
|
|
80
88
|
}]);
|
|
81
89
|
|
|
82
90
|
if (installPython) {
|
|
83
91
|
await installPythonDeps(missingPython);
|
|
84
92
|
}
|
|
93
|
+
} else if (missingPython.length > 0 && (!hasPython || !hasPip)) {
|
|
94
|
+
console.log(chalk.yellow('\nā ļø Python packages are needed but Python/pip is not available.'));
|
|
95
|
+
console.log(chalk.gray('Install Python first, then run: pip3 install --user ' + missingPython.map(p => p.name).join(' ')));
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -216,12 +227,15 @@ export async function init(options) {
|
|
|
216
227
|
try {
|
|
217
228
|
const result = await setupHooks(projectPath);
|
|
218
229
|
if (result) {
|
|
219
|
-
hooksSpinner.succeed('Hooks configured');
|
|
230
|
+
hooksSpinner.succeed('Hooks configured (with retry limits & graceful failure)');
|
|
231
|
+
console.log(chalk.gray(' ā Auto-updates docs graph when editing plans/'));
|
|
232
|
+
console.log(chalk.gray(' ā Max 3 retries, 5s timeout, non-blocking failures'));
|
|
220
233
|
} else {
|
|
221
234
|
hooksSpinner.info('Hooks skipped');
|
|
222
235
|
}
|
|
223
236
|
} catch (e) {
|
|
224
237
|
hooksSpinner.warn('Failed to set up hooks');
|
|
238
|
+
console.log(chalk.yellow(' ā You can configure hooks manually later'));
|
|
225
239
|
}
|
|
226
240
|
}
|
|
227
241
|
|
|
@@ -245,13 +259,42 @@ export async function init(options) {
|
|
|
245
259
|
gitSpinner.warn('Failed to update .gitignore');
|
|
246
260
|
}
|
|
247
261
|
|
|
248
|
-
// Step 11:
|
|
262
|
+
// Step 11: Offer to install optional tools
|
|
263
|
+
if (!options.noDeps && globalDeps.optional) {
|
|
264
|
+
const missingOptional = globalDeps.optional.filter(d => !d.found);
|
|
265
|
+
|
|
266
|
+
if (missingOptional.length > 0 && !options.yes) {
|
|
267
|
+
console.log(chalk.cyan('\nš¦ Optional Tools\n'));
|
|
268
|
+
console.log(chalk.gray('These tools enhance skills but are not required. Select which ones to install:\n'));
|
|
269
|
+
|
|
270
|
+
const { selectedTools } = await inquirer.prompt([{
|
|
271
|
+
type: 'checkbox',
|
|
272
|
+
name: 'selectedTools',
|
|
273
|
+
message: 'Select optional tools to install:',
|
|
274
|
+
choices: missingOptional.map(tool => ({
|
|
275
|
+
name: `${tool.name.padEnd(8)} - ${tool.purpose} ${chalk.gray(`(${tool.usedBy.join(', ')})`)}`,
|
|
276
|
+
value: tool.name,
|
|
277
|
+
// Check all by default except 'gh' (GitHub CLI)
|
|
278
|
+
checked: tool.name !== 'gh'
|
|
279
|
+
}))
|
|
280
|
+
}]);
|
|
281
|
+
|
|
282
|
+
if (selectedTools.length > 0) {
|
|
283
|
+
const toolsToInstall = missingOptional.filter(t => selectedTools.includes(t.name));
|
|
284
|
+
await installOptionalTools(toolsToInstall, globalDeps.os);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(chalk.gray('No optional tools selected. Skipping...\n'));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Step 12: Project setup (package manager, .env, node version)
|
|
249
292
|
console.log('\n' + chalk.cyan('ā'.repeat(40)));
|
|
250
293
|
console.log(chalk.bold('š¦ Project Setup\n'));
|
|
251
294
|
|
|
252
295
|
const setupResult = await runProjectSetup(projectPath, { yes: options.yes });
|
|
253
296
|
|
|
254
|
-
// Step
|
|
297
|
+
// Step 13: Check skill-specific project dependencies
|
|
255
298
|
if (!options.noDeps) {
|
|
256
299
|
console.log('');
|
|
257
300
|
const projectSpinner = ora('Checking skill dependencies...').start();
|
|
@@ -262,9 +305,25 @@ export async function init(options) {
|
|
|
262
305
|
console.log(chalk.yellow('\nā ļø Some optional dependencies for skills are missing:\n'));
|
|
263
306
|
|
|
264
307
|
for (const dep of projectDeps.missing) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
308
|
+
const pm = setupResult.packageManager?.name || 'npm';
|
|
309
|
+
let cmd;
|
|
310
|
+
if (dep.type === 'pip') {
|
|
311
|
+
cmd = chalk.cyan(`pip install ${dep.name}`);
|
|
312
|
+
} else {
|
|
313
|
+
switch (pm) {
|
|
314
|
+
case 'pnpm':
|
|
315
|
+
cmd = chalk.cyan(`pnpm add -D ${dep.name}`);
|
|
316
|
+
break;
|
|
317
|
+
case 'yarn':
|
|
318
|
+
cmd = chalk.cyan(`yarn add -D ${dep.name}`);
|
|
319
|
+
break;
|
|
320
|
+
case 'bun':
|
|
321
|
+
cmd = chalk.cyan(`bun add -D ${dep.name}`);
|
|
322
|
+
break;
|
|
323
|
+
default:
|
|
324
|
+
cmd = chalk.cyan(`npm install -D ${dep.name}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
268
327
|
console.log(` ${dep.name} - ${dep.purpose}`);
|
|
269
328
|
console.log(` ${cmd}\n`);
|
|
270
329
|
}
|
|
@@ -278,7 +337,12 @@ export async function init(options) {
|
|
|
278
337
|
}]);
|
|
279
338
|
|
|
280
339
|
if (install) {
|
|
281
|
-
|
|
340
|
+
// Use package manager-aware installation if we have one
|
|
341
|
+
if (setupResult.packageManager) {
|
|
342
|
+
await installProjectDepsWithPM(projectPath, projectDeps.missing, setupResult.packageManager);
|
|
343
|
+
} else {
|
|
344
|
+
await installProjectDeps(projectPath, projectDeps.missing);
|
|
345
|
+
}
|
|
282
346
|
}
|
|
283
347
|
}
|
|
284
348
|
}
|
|
@@ -323,6 +387,14 @@ export async function init(options) {
|
|
|
323
387
|
stepNum++;
|
|
324
388
|
console.log(` ${stepNum}. See all skills: ${chalk.cyan('/help')}`);
|
|
325
389
|
console.log('');
|
|
326
|
-
console.log(chalk.gray('Docs:
|
|
390
|
+
console.log(chalk.gray('Docs:'));
|
|
391
|
+
console.log(chalk.gray(' ⢠Skills: .claude/skills/_registry.md'));
|
|
392
|
+
console.log(chalk.gray(' ⢠Hooks: .claude/hooks-guide.md (troubleshooting hook failures)'));
|
|
327
393
|
console.log('');
|
|
394
|
+
|
|
395
|
+
// Add note about hooks if they were set up
|
|
396
|
+
if (!options.noHooks) {
|
|
397
|
+
console.log(chalk.dim('š” Tip: If hooks fail repeatedly, see .claude/hooks-guide.md for troubleshooting'));
|
|
398
|
+
console.log('');
|
|
399
|
+
}
|
|
328
400
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -46,15 +46,27 @@ const DEFAULT_SETTINGS = {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Hooks for automatic graph updates
|
|
49
|
+
*
|
|
50
|
+
* IMPORTANT: Hooks should be resilient and fail gracefully.
|
|
51
|
+
* - Use `|| true` to prevent hook failures from blocking Claude
|
|
52
|
+
* - Add retry limits to prevent infinite loops
|
|
53
|
+
* - Keep hooks fast (< 2 seconds) to avoid slowing down workflow
|
|
49
54
|
*/
|
|
50
55
|
const DEFAULT_HOOKS = {
|
|
51
56
|
postToolUse: [
|
|
52
57
|
{
|
|
53
58
|
matcher: {
|
|
54
59
|
toolName: "Write|Edit",
|
|
55
|
-
path: "plans
|
|
60
|
+
path: "plans/**/*.md" // Only trigger on .md files in plans/
|
|
61
|
+
},
|
|
62
|
+
command: "bash .claude/scripts/safe-graph-update.sh $PATH",
|
|
63
|
+
// Retry configuration (Claude Code built-in support)
|
|
64
|
+
retries: {
|
|
65
|
+
maxAttempts: 3, // Max 3 attempts total
|
|
66
|
+
backoff: "exponential", // Wait longer between retries
|
|
67
|
+
failureAction: "warn" // Show warning but don't block
|
|
56
68
|
},
|
|
57
|
-
|
|
69
|
+
timeout: 5000 // 5 second timeout per attempt
|
|
58
70
|
}
|
|
59
71
|
]
|
|
60
72
|
};
|
|
@@ -93,8 +105,24 @@ export async function setupHooks(projectPath, options = {}) {
|
|
|
93
105
|
|
|
94
106
|
const claudePath = path.join(projectPath, '.claude');
|
|
95
107
|
const hooksPath = path.join(claudePath, 'hooks.json');
|
|
108
|
+
const claudeScriptsPath = path.join(claudePath, 'scripts');
|
|
96
109
|
|
|
97
110
|
await fs.ensureDir(claudePath);
|
|
111
|
+
await fs.ensureDir(claudeScriptsPath);
|
|
112
|
+
|
|
113
|
+
// Copy safe-graph-update.sh wrapper script to .claude/scripts/
|
|
114
|
+
const wrapperSource = path.join(TEMPLATES_PATH, 'scripts', 'safe-graph-update.sh');
|
|
115
|
+
const wrapperDest = path.join(claudeScriptsPath, 'safe-graph-update.sh');
|
|
116
|
+
|
|
117
|
+
if (await fs.pathExists(wrapperSource)) {
|
|
118
|
+
await fs.copy(wrapperSource, wrapperDest);
|
|
119
|
+
// Make executable
|
|
120
|
+
try {
|
|
121
|
+
await fs.chmod(wrapperDest, 0o755);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// chmod might fail on Windows, that's ok
|
|
124
|
+
}
|
|
125
|
+
}
|
|
98
126
|
|
|
99
127
|
let hooks = DEFAULT_HOOKS;
|
|
100
128
|
|
|
@@ -108,8 +136,23 @@ export async function setupHooks(projectPath, options = {}) {
|
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
|
|
139
|
+
// Update command to use safe wrapper if it exists
|
|
140
|
+
if (hooks.postToolUse && hooks.postToolUse[0]) {
|
|
141
|
+
if (await fs.pathExists(wrapperDest)) {
|
|
142
|
+
hooks.postToolUse[0].command = "bash .claude/scripts/safe-graph-update.sh $PATH";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
111
146
|
await fs.writeJson(hooksPath, hooks, { spaces: 2 });
|
|
112
147
|
|
|
148
|
+
// Copy hooks guide documentation
|
|
149
|
+
const hooksGuideSource = path.join(TEMPLATES_PATH, 'hooks-guide.md');
|
|
150
|
+
const hooksGuideDest = path.join(claudePath, 'hooks-guide.md');
|
|
151
|
+
|
|
152
|
+
if (await fs.pathExists(hooksGuideSource)) {
|
|
153
|
+
await fs.copy(hooksGuideSource, hooksGuideDest);
|
|
154
|
+
}
|
|
155
|
+
|
|
113
156
|
return { path: hooksPath, hooks };
|
|
114
157
|
}
|
|
115
158
|
|
|
@@ -218,8 +261,16 @@ export async function updateGitignore(projectPath) {
|
|
|
218
261
|
|
|
219
262
|
const entriesToAdd = [
|
|
220
263
|
'',
|
|
221
|
-
'# Claude Code',
|
|
264
|
+
'# Claude Code - Settings and local configuration',
|
|
222
265
|
'.claude/settings.local.json',
|
|
266
|
+
'',
|
|
267
|
+
'# Claude Code - Installed skills, knowledge, and scripts',
|
|
268
|
+
'# These are managed by @codihaus/claude-skills package',
|
|
269
|
+
'.claude/skills/',
|
|
270
|
+
'.claude/knowledge/',
|
|
271
|
+
'.claude/scripts/',
|
|
272
|
+
'',
|
|
273
|
+
'# Environment files',
|
|
223
274
|
'.env',
|
|
224
275
|
'*.env.local',
|
|
225
276
|
''
|
package/src/utils/deps.js
CHANGED
|
@@ -39,7 +39,7 @@ function getInstallHint(tool, osType) {
|
|
|
39
39
|
const hints = {
|
|
40
40
|
node: {
|
|
41
41
|
macos: 'brew install node OR https://nodejs.org/',
|
|
42
|
-
debian: 'sudo apt install nodejs npm OR https://nodejs.org/',
|
|
42
|
+
debian: 'sudo apt update && sudo apt install nodejs npm OR https://nodejs.org/',
|
|
43
43
|
redhat: 'sudo dnf install nodejs npm OR https://nodejs.org/',
|
|
44
44
|
arch: 'sudo pacman -S nodejs npm',
|
|
45
45
|
linux: 'https://nodejs.org/ (use official installer)',
|
|
@@ -47,7 +47,7 @@ function getInstallHint(tool, osType) {
|
|
|
47
47
|
},
|
|
48
48
|
git: {
|
|
49
49
|
macos: 'xcode-select --install OR brew install git',
|
|
50
|
-
debian: 'sudo apt install git',
|
|
50
|
+
debian: 'sudo apt update && sudo apt install git',
|
|
51
51
|
redhat: 'sudo dnf install git',
|
|
52
52
|
arch: 'sudo pacman -S git',
|
|
53
53
|
linux: 'sudo apt install git OR sudo dnf install git',
|
|
@@ -55,19 +55,19 @@ function getInstallHint(tool, osType) {
|
|
|
55
55
|
},
|
|
56
56
|
python3: {
|
|
57
57
|
macos: 'brew install python3 OR https://python.org/',
|
|
58
|
-
debian: 'sudo apt install python3 python3-pip',
|
|
58
|
+
debian: 'sudo apt update && sudo apt install python3 python3-pip',
|
|
59
59
|
redhat: 'sudo dnf install python3 python3-pip',
|
|
60
60
|
arch: 'sudo pacman -S python python-pip',
|
|
61
|
-
linux: 'sudo apt install python3 python3-pip',
|
|
61
|
+
linux: 'sudo apt install python3 python3-pip OR sudo dnf install python3 python3-pip',
|
|
62
62
|
unknown: 'https://python.org/'
|
|
63
63
|
},
|
|
64
64
|
pip3: {
|
|
65
|
-
macos: 'python3 -m ensurepip OR brew install python3',
|
|
66
|
-
debian: 'sudo apt install python3-pip',
|
|
65
|
+
macos: 'python3 -m ensurepip --upgrade OR brew install python3',
|
|
66
|
+
debian: 'sudo apt update && sudo apt install python3-pip',
|
|
67
67
|
redhat: 'sudo dnf install python3-pip',
|
|
68
68
|
arch: 'sudo pacman -S python-pip',
|
|
69
|
-
linux: 'sudo apt install python3-pip',
|
|
70
|
-
unknown: 'python3 -m ensurepip'
|
|
69
|
+
linux: 'sudo apt install python3-pip OR python3 -m ensurepip --upgrade',
|
|
70
|
+
unknown: 'python3 -m ensurepip --upgrade'
|
|
71
71
|
},
|
|
72
72
|
jq: {
|
|
73
73
|
macos: 'brew install jq',
|
|
@@ -236,6 +236,7 @@ export const PROJECT_DEPS = {
|
|
|
236
236
|
forTesting: [
|
|
237
237
|
{
|
|
238
238
|
name: '@playwright/test',
|
|
239
|
+
type: 'npm',
|
|
239
240
|
purpose: 'UI testing with /dev-test',
|
|
240
241
|
usedBy: ['/dev-test', '/dev-coding-frontend']
|
|
241
242
|
}
|
|
@@ -243,6 +244,7 @@ export const PROJECT_DEPS = {
|
|
|
243
244
|
optional: [
|
|
244
245
|
{
|
|
245
246
|
name: '@mermaid-js/mermaid-cli',
|
|
247
|
+
type: 'npm',
|
|
246
248
|
purpose: 'Diagram rendering (optional)',
|
|
247
249
|
usedBy: ['/utils/diagram'],
|
|
248
250
|
global: true
|
|
@@ -539,9 +541,18 @@ export async function installPythonDeps(packages) {
|
|
|
539
541
|
for (const pkg of packages) {
|
|
540
542
|
try {
|
|
541
543
|
console.log(` Installing ${pkg.name}...`);
|
|
542
|
-
|
|
544
|
+
|
|
545
|
+
// Try without sudo first (user install)
|
|
546
|
+
try {
|
|
547
|
+
execSync(`pip3 install --user ${pkg.name}`, { stdio: 'inherit' });
|
|
548
|
+
} catch (userError) {
|
|
549
|
+
// If user install fails, try with sudo (system-wide)
|
|
550
|
+
console.log(chalk.yellow(` User install failed, trying with sudo...`));
|
|
551
|
+
execSync(`sudo pip3 install ${pkg.name}`, { stdio: 'inherit' });
|
|
552
|
+
}
|
|
543
553
|
} catch (e) {
|
|
544
554
|
console.log(chalk.red(` Failed to install ${pkg.name}`));
|
|
555
|
+
console.log(chalk.gray(` Try manually: pip3 install --user ${pkg.name}`));
|
|
545
556
|
return false;
|
|
546
557
|
}
|
|
547
558
|
}
|
|
@@ -549,6 +560,150 @@ export async function installPythonDeps(packages) {
|
|
|
549
560
|
return true;
|
|
550
561
|
}
|
|
551
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Install missing optional tools
|
|
565
|
+
*/
|
|
566
|
+
export async function installOptionalTools(tools, osType) {
|
|
567
|
+
if (tools.length === 0) return true;
|
|
568
|
+
|
|
569
|
+
console.log(chalk.cyan('\nInstalling optional tools...\n'));
|
|
570
|
+
|
|
571
|
+
const installCommands = {
|
|
572
|
+
gh: {
|
|
573
|
+
macos: 'brew install gh',
|
|
574
|
+
debian: 'curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y',
|
|
575
|
+
redhat: 'sudo dnf install gh -y',
|
|
576
|
+
arch: 'sudo pacman -S github-cli --noconfirm',
|
|
577
|
+
fallback: 'npm install -g gh'
|
|
578
|
+
},
|
|
579
|
+
mmdc: {
|
|
580
|
+
all: 'npm install -g @mermaid-js/mermaid-cli'
|
|
581
|
+
},
|
|
582
|
+
tree: {
|
|
583
|
+
macos: 'brew install tree',
|
|
584
|
+
debian: 'sudo apt install tree -y',
|
|
585
|
+
redhat: 'sudo dnf install tree -y',
|
|
586
|
+
arch: 'sudo pacman -S tree --noconfirm'
|
|
587
|
+
},
|
|
588
|
+
scc: {
|
|
589
|
+
macos: 'brew install scc',
|
|
590
|
+
debian: 'go install github.com/boyter/scc/v3@latest',
|
|
591
|
+
redhat: 'go install github.com/boyter/scc/v3@latest',
|
|
592
|
+
arch: 'yay -S scc --noconfirm || go install github.com/boyter/scc/v3@latest',
|
|
593
|
+
fallback: 'go install github.com/boyter/scc/v3@latest'
|
|
594
|
+
},
|
|
595
|
+
rg: {
|
|
596
|
+
macos: 'brew install ripgrep',
|
|
597
|
+
debian: 'sudo apt install ripgrep -y',
|
|
598
|
+
redhat: 'sudo dnf install ripgrep -y',
|
|
599
|
+
arch: 'sudo pacman -S ripgrep --noconfirm'
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
let successCount = 0;
|
|
604
|
+
let failCount = 0;
|
|
605
|
+
|
|
606
|
+
for (const tool of tools) {
|
|
607
|
+
const commands = installCommands[tool.name];
|
|
608
|
+
if (!commands) {
|
|
609
|
+
console.log(chalk.gray(` ā ${tool.name} - no auto-install available`));
|
|
610
|
+
console.log(chalk.gray(` ā ${tool.installHint}\n`));
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let command = commands.all || commands[osType] || commands.fallback;
|
|
615
|
+
|
|
616
|
+
if (!command) {
|
|
617
|
+
console.log(chalk.gray(` ā ${tool.name} - no auto-install available for ${osType}`));
|
|
618
|
+
console.log(chalk.gray(` ā ${tool.installHint}\n`));
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
console.log(` Installing ${chalk.cyan(tool.name)}...`);
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
execSync(command, {
|
|
626
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
627
|
+
encoding: 'utf-8'
|
|
628
|
+
});
|
|
629
|
+
console.log(chalk.green(` ā ${tool.name} installed successfully\n`));
|
|
630
|
+
successCount++;
|
|
631
|
+
} catch (e) {
|
|
632
|
+
// Try fallback if available
|
|
633
|
+
if (commands.fallback && command !== commands.fallback) {
|
|
634
|
+
console.log(chalk.yellow(` Primary method failed, trying fallback...`));
|
|
635
|
+
try {
|
|
636
|
+
execSync(commands.fallback, {
|
|
637
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
638
|
+
encoding: 'utf-8'
|
|
639
|
+
});
|
|
640
|
+
console.log(chalk.green(` ā ${tool.name} installed successfully\n`));
|
|
641
|
+
successCount++;
|
|
642
|
+
continue;
|
|
643
|
+
} catch (fallbackError) {
|
|
644
|
+
// Fallback also failed
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
console.log(chalk.red(` ā Failed to install ${tool.name}`));
|
|
649
|
+
console.log(chalk.gray(` Try manually: ${tool.installHint}\n`));
|
|
650
|
+
failCount++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (successCount > 0) {
|
|
655
|
+
console.log(chalk.green(`\nā Installed ${successCount} optional tools`));
|
|
656
|
+
}
|
|
657
|
+
if (failCount > 0) {
|
|
658
|
+
console.log(chalk.yellow(`ā Failed to install ${failCount} optional tools`));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return failCount === 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Install project dependencies with package manager detection
|
|
666
|
+
*/
|
|
667
|
+
export async function installProjectDepsWithPM(projectPath, deps, packageManager) {
|
|
668
|
+
const npmDeps = deps.filter(d => !d.global && d.type !== 'pip');
|
|
669
|
+
|
|
670
|
+
if (npmDeps.length === 0) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
console.log(chalk.cyan(`\nInstalling npm dependencies with ${packageManager.name}...`));
|
|
675
|
+
const names = npmDeps.map(d => d.name).join(' ');
|
|
676
|
+
|
|
677
|
+
// Build install command based on package manager
|
|
678
|
+
let installCmd;
|
|
679
|
+
switch (packageManager.name) {
|
|
680
|
+
case 'pnpm':
|
|
681
|
+
installCmd = `pnpm add -D ${names}`;
|
|
682
|
+
break;
|
|
683
|
+
case 'yarn':
|
|
684
|
+
installCmd = `yarn add -D ${names}`;
|
|
685
|
+
break;
|
|
686
|
+
case 'bun':
|
|
687
|
+
installCmd = `bun add -D ${names}`;
|
|
688
|
+
break;
|
|
689
|
+
case 'npm':
|
|
690
|
+
default:
|
|
691
|
+
installCmd = `npm install -D ${names}`;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
execSync(installCmd, {
|
|
697
|
+
cwd: projectPath,
|
|
698
|
+
stdio: 'inherit'
|
|
699
|
+
});
|
|
700
|
+
return true;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
console.log(chalk.red(`Failed to install npm dependencies with ${packageManager.name}`));
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
552
707
|
/**
|
|
553
708
|
* Get summary of what's missing
|
|
554
709
|
*/
|
package/src/utils/skills.js
CHANGED
|
@@ -198,6 +198,22 @@ export async function copySkillsToProject(projectPath, skillNames = null) {
|
|
|
198
198
|
} else {
|
|
199
199
|
// Copy directory
|
|
200
200
|
await fs.copy(skill.path, targetSkillPath);
|
|
201
|
+
|
|
202
|
+
// Make scripts within skill folder executable
|
|
203
|
+
const skillScriptsPath = path.join(targetSkillPath, 'scripts');
|
|
204
|
+
if (await fs.pathExists(skillScriptsPath)) {
|
|
205
|
+
const scriptFiles = await fs.readdir(skillScriptsPath);
|
|
206
|
+
for (const scriptFile of scriptFiles) {
|
|
207
|
+
const scriptPath = path.join(skillScriptsPath, scriptFile);
|
|
208
|
+
const stat = await fs.stat(scriptPath);
|
|
209
|
+
if (stat.isFile()) {
|
|
210
|
+
const isScript = scriptFile.endsWith('.py') || scriptFile.endsWith('.sh') || scriptFile.endsWith('.js');
|
|
211
|
+
if (isScript) {
|
|
212
|
+
await fs.chmod(scriptPath, 0o755);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
copied.push(skill.name);
|
|
@@ -326,6 +342,17 @@ export async function copyScriptsToProject(projectPath) {
|
|
|
326
342
|
|
|
327
343
|
try {
|
|
328
344
|
await fs.copy(sourcePath, targetItemPath);
|
|
345
|
+
|
|
346
|
+
// Make scripts executable (chmod +x)
|
|
347
|
+
const stat = await fs.stat(targetItemPath);
|
|
348
|
+
if (stat.isFile()) {
|
|
349
|
+
// Check if it's a script file
|
|
350
|
+
const isScript = item.endsWith('.py') || item.endsWith('.sh') || item.endsWith('.js');
|
|
351
|
+
if (isScript) {
|
|
352
|
+
await fs.chmod(targetItemPath, 0o755);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
329
356
|
copied.push(item);
|
|
330
357
|
} catch (e) {
|
|
331
358
|
errors.push({ name: item, error: e.message });
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Hook Trigger Test Cases
|
|
2
|
+
|
|
3
|
+
Test cases for `path: "plans/**/*.md"` matcher.
|
|
4
|
+
|
|
5
|
+
## ā
WILL Trigger (Correct)
|
|
6
|
+
|
|
7
|
+
These files SHOULD trigger the docs-graph update hook:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
plans/brd/README.md ā
|
|
11
|
+
plans/brd/context.md ā
|
|
12
|
+
plans/brd/use-cases/auth/UC-AUTH-001-login.md ā
|
|
13
|
+
plans/brd/changes/CR-001-add-sso.md ā
|
|
14
|
+
plans/features/auth/README.md ā
|
|
15
|
+
plans/features/auth/scout.md ā
|
|
16
|
+
plans/features/auth/specs/README.md ā
|
|
17
|
+
plans/features/auth/specs/UC-AUTH-001/README.md ā
|
|
18
|
+
plans/scout/README.md ā
|
|
19
|
+
plans/scout/structure.md ā
|
|
20
|
+
plans/scout/stack.md ā
|
|
21
|
+
plans/docs-graph.md ā
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## ā Will NOT Trigger (Correct - Performance Optimization)
|
|
25
|
+
|
|
26
|
+
These files should NOT trigger the hook (saves resources):
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
plans/features/auth/questionnaire-2024-01-20.xlsx ā (Excel file)
|
|
30
|
+
plans/scout/screenshots/home.png ā (Image file)
|
|
31
|
+
plans/scout/screenshots/dashboard.png ā (Image file)
|
|
32
|
+
plans/docs-graph.json ā (JSON file, processed separately)
|
|
33
|
+
plans/features/billing/architecture.json ā (JSON file)
|
|
34
|
+
plans/.gitkeep ā (No extension)
|
|
35
|
+
plans/brd/references.txt ā (Text file)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Pattern Explanation
|
|
39
|
+
|
|
40
|
+
**Pattern:** `plans/**/*.md`
|
|
41
|
+
|
|
42
|
+
- `plans/` - Must be in plans directory
|
|
43
|
+
- `**/` - Can be at any depth (0 or more subdirectories)
|
|
44
|
+
- `*.md` - Must end with .md extension
|
|
45
|
+
|
|
46
|
+
**Double Protection:**
|
|
47
|
+
|
|
48
|
+
1. **Hook matcher** (Claude Code level):
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"matcher": {
|
|
52
|
+
"toolName": "Write|Edit",
|
|
53
|
+
"path": "plans/**/*.md"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. **Script validation** (Bash level):
|
|
59
|
+
```bash
|
|
60
|
+
if [[ ! "$FILE_PATH" =~ ^plans/.*\.md$ ]]; then
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Testing
|
|
66
|
+
|
|
67
|
+
To test if a file will trigger the hook:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Test pattern matching
|
|
71
|
+
echo "plans/brd/README.md" | grep -E '^plans/.*\.md$'
|
|
72
|
+
# Output: plans/brd/README.md ā Matches!
|
|
73
|
+
|
|
74
|
+
echo "plans/scout/screenshots/home.png" | grep -E '^plans/.*\.md$'
|
|
75
|
+
# No output ā Doesn't match
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Performance Impact
|
|
79
|
+
|
|
80
|
+
**Before** (with `plans/**/*`):
|
|
81
|
+
- 100 files changed in plans/
|
|
82
|
+
- Hook runs 100 times
|
|
83
|
+
- Includes: 70 .md, 20 .png, 5 .xlsx, 5 .json
|
|
84
|
+
- Wastes 30% of hook runs
|
|
85
|
+
|
|
86
|
+
**After** (with `plans/**/*.md`):
|
|
87
|
+
- 100 files changed in plans/
|
|
88
|
+
- Hook runs 70 times
|
|
89
|
+
- Includes: 70 .md only
|
|
90
|
+
- Saves 30% of hook runs
|
|
91
|
+
|
|
92
|
+
## Common Gotchas
|
|
93
|
+
|
|
94
|
+
ā **Don't match root .md files:**
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"matcher": {
|
|
98
|
+
"path": "*.md" // ā Bad: matches README.md in root
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
ā **Don't match all .md everywhere:**
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"matcher": {
|
|
107
|
+
"path": "**/*.md" // ā Bad: matches node_modules/**/*.md
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
ā
**Do scope to specific directories:**
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"matcher": {
|
|
116
|
+
"path": "plans/**/*.md" // ā Good: only plans/
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Verification After Init
|
|
122
|
+
|
|
123
|
+
After running `npx @codihaus/claude-skills init`, verify:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Check hook configuration
|
|
127
|
+
cat .claude/hooks.json | jq '.postToolUse[0].matcher.path'
|
|
128
|
+
# Should output: "plans/**/*.md"
|
|
129
|
+
|
|
130
|
+
cat .claude/hooks.json | jq '.postToolUse[0].command'
|
|
131
|
+
# Should output: "bash .claude/scripts/safe-graph-update.sh $PATH"
|
|
132
|
+
|
|
133
|
+
# Test the safe wrapper script
|
|
134
|
+
bash .claude/scripts/safe-graph-update.sh plans/brd/README.md
|
|
135
|
+
# Should run graph.py
|
|
136
|
+
|
|
137
|
+
bash .claude/scripts/safe-graph-update.sh plans/screenshot.png
|
|
138
|
+
# Should exit early (not run graph.py)
|
|
139
|
+
|
|
140
|
+
# Check that script exists in .claude/scripts/
|
|
141
|
+
ls -la .claude/scripts/safe-graph-update.sh
|
|
142
|
+
# Should show the file with executable permissions
|
|
143
|
+
```
|