claude-devkit-cli 1.3.5 → 1.4.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/README.md CHANGED
@@ -109,7 +109,16 @@ cd my-project
109
109
  claude-devkit init .
110
110
  ```
111
111
 
112
- **Option C: Force re-install** (overwrites existing files)
112
+ **Option C: Global skills install** (available in all projects without running `init` again)
113
+
114
+ ```bash
115
+ claude-devkit init --global
116
+ # or after per-project init, answer "yes" to the global prompt
117
+ ```
118
+
119
+ Skills installed globally at `~/.claude/skills/` are available in every project. Per-project `.claude/skills/` always takes precedence over global — so projects can still override individual skills.
120
+
121
+ **Option D: Force re-install** (overwrites existing files)
113
122
 
114
123
  ```bash
115
124
  npx claude-devkit-cli init --force .
@@ -118,7 +127,7 @@ npx claude-devkit-cli init --force .
118
127
  **Option D: Selective install** (only specific components)
119
128
 
120
129
  ```bash
121
- npx claude-devkit-cli init --only hooks,commands .
130
+ npx claude-devkit-cli init --only hooks,skills .
122
131
  ```
123
132
 
124
133
  ### What Gets Installed
@@ -135,13 +144,13 @@ your-project/
135
144
  │ │ ├── comment-guard.js ← Blocks placeholder comments
136
145
  │ │ ├── sensitive-guard.sh ← Blocks access to secrets
137
146
  │ │ └── self-review.sh ← Quality checklist on stop
138
- │ └── commands/
139
- │ ├── mf-plan.md ← /mf-plan command
140
- │ ├── mf-challenge.md ← /mf-challenge command
141
- │ ├── mf-build.md ← /mf-build command
142
- │ ├── mf-fix.md ← /mf-fix command
143
- │ ├── mf-review.md ← /mf-review command
144
- │ └── mf-commit.md ← /mf-commit command
147
+ │ └── skills/
148
+ │ ├── mf-plan/SKILL.md ← /mf-plan skill
149
+ │ ├── mf-challenge/SKILL.md ← /mf-challenge skill
150
+ │ ├── mf-build/SKILL.md ← /mf-build skill
151
+ │ ├── mf-fix/SKILL.md ← /mf-fix skill
152
+ │ ├── mf-review/SKILL.md ← /mf-review skill
153
+ │ └── mf-commit/SKILL.md ← /mf-commit skill
145
154
  ├── scripts/
146
155
  │ └── build-test.sh ← Universal test runner
147
156
  └── docs/
@@ -187,7 +196,7 @@ npx claude-devkit-cli list
187
196
  npx claude-devkit-cli remove
188
197
  ```
189
198
 
190
- This removes hooks, commands, settings, and build-test.sh. It preserves `CLAUDE.md` (which you may have customized) and `docs/` (which contains your specs).
199
+ This removes hooks, skills, settings, and build-test.sh. It preserves `CLAUDE.md` (which you may have customized) and `docs/` (which contains your specs).
191
200
 
192
201
  ---
193
202
 
@@ -836,12 +845,12 @@ Add project-specific rules to `.claude/CLAUDE.md`:
836
845
  - All strings must be localized via i18n keys
837
846
  ```
838
847
 
839
- ### Adding Custom Commands
848
+ ### Adding Custom Skills
840
849
 
841
- Create new `.md` files in `.claude/commands/`:
850
+ Create new skills in `.claude/skills/<name>/SKILL.md`:
842
851
 
843
852
  ```markdown
844
- # .claude/commands/deploy.md
853
+ # .claude/skills/deploy/SKILL.md
845
854
 
846
855
  Run the deployment pipeline:
847
856
  1. /mf-review
@@ -948,8 +957,8 @@ A: This is intentionally not a command (it's expensive and rare). When needed, p
948
957
  **Q: What if my project uses multiple languages?**
949
958
  A: `build-test.sh` detects the first match. For monorepos, you may need to run it from each sub-project directory or customize the script.
950
959
 
951
- **Q: Can I add more commands?**
952
- A: Yes. Drop a `.md` file in `.claude/commands/` and it becomes available as a slash command. See [Customization](#9-customization).
960
+ **Q: Can I add more skills?**
961
+ A: Yes. Create a directory `.claude/skills/<name>/SKILL.md` and it becomes available as a slash command. See [Customization](#9-customization).
953
962
 
954
963
  **Q: How do I update the kit in existing projects?**
955
964
  A: Run `npx claude-devkit-cli upgrade`. It automatically detects which files you've customized and only updates unchanged files. Use `--force` to overwrite everything.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-devkit-cli",
3
- "version": "1.3.5",
3
+ "version": "1.4.0",
4
4
  "description": "CLI toolkit for spec-first development with Claude Code — hooks, commands, guards, and test runners",
5
5
  "bin": {
6
6
  "claude-devkit": "./bin/devkit.js",
package/src/cli.js CHANGED
@@ -18,7 +18,8 @@ export function cli(argv) {
18
18
  .command('init [path]')
19
19
  .description('Initialize a project with the dev-kit')
20
20
  .option('-f, --force', 'Overwrite existing files')
21
- .option('--only <components>', 'Install only specific components (comma-separated: hooks,commands,scripts,docs,config)')
21
+ .option('-g, --global', 'Install skills globally to ~/.claude/skills/ (available in all projects)')
22
+ .option('--only <components>', 'Install only specific components (comma-separated: hooks,skills,scripts,docs,config)')
22
23
  .option('--adopt', 'Adopt existing kit files without overwriting (migration from setup.sh)')
23
24
  .option('--dry-run', 'Show what would be done without making changes')
24
25
  .action(async (path, opts) => {
@@ -30,6 +31,7 @@ export function cli(argv) {
30
31
  .command('upgrade [path]')
31
32
  .description('Smart upgrade — preserves customized files')
32
33
  .option('-f, --force', 'Overwrite even customized files')
34
+ .option('-g, --global', 'Upgrade skills globally in ~/.claude/skills/')
33
35
  .option('--dry-run', 'Show what would be done without making changes')
34
36
  .action(async (path, opts) => {
35
37
  const { upgradeCommand } = await import('./commands/upgrade.js');
@@ -63,9 +65,10 @@ export function cli(argv) {
63
65
  program
64
66
  .command('remove [path]')
65
67
  .description('Uninstall dev-kit (preserves CLAUDE.md and docs/)')
66
- .action(async (path) => {
68
+ .option('-g, --global', 'Remove global install (~/.claude/skills/, ~/.claude/hooks/, hook entries from ~/.claude/settings.json)')
69
+ .action(async (path, opts) => {
67
70
  const { removeCommand } = await import('./commands/remove.js');
68
- await removeCommand(path || '.');
71
+ await removeCommand(path || '.', opts);
69
72
  });
70
73
 
71
74
  program.parse(argv);
@@ -1,19 +1,96 @@
1
- import { resolve } from 'node:path';
1
+ import { resolve, join, dirname } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { readFileSync } from 'node:fs';
5
- import { dirname } from 'node:path';
6
5
  import { fileURLToPath } from 'node:url';
7
6
  import { log } from '../lib/logger.js';
8
7
  import { detectProject } from '../lib/detector.js';
9
8
  import { readManifest, writeManifest, createManifest, setFileEntry } from '../lib/manifest.js';
10
9
  import { hashFile } from '../lib/hasher.js';
10
+ import { homedir } from 'node:os';
11
+ import { mkdir } from 'node:fs/promises';
11
12
  import {
12
13
  getAllFiles, getFilesForComponents, installFile,
13
14
  ensurePlaceholderDir, setPermissions, fillTemplate,
14
15
  verifySettingsJson, PLACEHOLDER_DIRS, COMPONENTS,
15
- getTemplateDir,
16
+ getTemplateDir, installSkillGlobal, getGlobalSkillsDir,
17
+ installHookGlobal, getGlobalHooksDir, mergeGlobalSettings,
16
18
  } from '../lib/installer.js';
19
+ import { readFile, writeFile } from 'node:fs/promises';
20
+
21
+ const GLOBAL_MANIFEST = join(homedir(), '.claude', '.devkit-manifest.json');
22
+
23
+ async function readGlobalManifest() {
24
+ try {
25
+ return JSON.parse(await readFile(GLOBAL_MANIFEST, 'utf-8'));
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ async function writeGlobalManifest(data) {
32
+ await mkdir(join(homedir(), '.claude'), { recursive: true });
33
+ await writeFile(GLOBAL_MANIFEST, JSON.stringify(data, null, 2) + '\n');
34
+ }
35
+
36
+ export async function initGlobal({ force = false, hooks = false } = {}) {
37
+ const globalSkillsDir = getGlobalSkillsDir();
38
+ await mkdir(globalSkillsDir, { recursive: true });
39
+
40
+ log.blank();
41
+ console.log('--- Installing global skills ---');
42
+
43
+ let copied = 0; let skipped = 0; let identical = 0;
44
+ for (const relPath of COMPONENTS.skills) {
45
+ const result = await installSkillGlobal(relPath, globalSkillsDir, { force });
46
+ if (result === 'copied') copied++;
47
+ else if (result === 'identical') identical++;
48
+ else skipped++;
49
+ }
50
+
51
+ const parts = [`${copied} copied`];
52
+ if (identical > 0) parts.push(`${identical} identical`);
53
+ if (skipped > 0) parts.push(`${skipped} customized (use --force to overwrite)`);
54
+ log.pass(`Global skills: ${parts.join(', ')}`);
55
+ log.info('Skills available in all projects via ~/.claude/skills/');
56
+
57
+ if (hooks) {
58
+ await initGlobalHooks({ force });
59
+ }
60
+
61
+ // Write global manifest
62
+ const existing = await readGlobalManifest() || {};
63
+ await writeGlobalManifest({
64
+ ...existing,
65
+ globalInstalled: true,
66
+ globalHooksInstalled: hooks || existing.globalHooksInstalled || false,
67
+ updatedAt: new Date().toISOString(),
68
+ });
69
+ }
70
+
71
+ export async function initGlobalHooks({ force = false } = {}) {
72
+ const globalHooksDir = getGlobalHooksDir();
73
+ await mkdir(globalHooksDir, { recursive: true });
74
+
75
+ log.blank();
76
+ console.log('--- Installing global hooks ---');
77
+
78
+ let copied = 0; let skipped = 0; let identical = 0;
79
+ for (const relPath of COMPONENTS.hooks) {
80
+ const result = await installHookGlobal(relPath, globalHooksDir, { force });
81
+ if (result === 'copied') copied++;
82
+ else if (result === 'identical') identical++;
83
+ else skipped++;
84
+ }
85
+
86
+ await mergeGlobalSettings(globalHooksDir);
87
+
88
+ const parts = [`${copied} copied`];
89
+ if (identical > 0) parts.push(`${identical} identical`);
90
+ if (skipped > 0) parts.push(`${skipped} customized (use --force to overwrite)`);
91
+ log.pass(`Global hooks: ${parts.join(', ')}`);
92
+ log.info('Hooks registered in ~/.claude/settings.json — active in all projects');
93
+ }
17
94
 
18
95
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
96
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8'));
@@ -30,6 +107,12 @@ export async function initCommand(path, opts) {
30
107
  log.info(`Target: ${targetDir}`);
31
108
  log.blank();
32
109
 
110
+ // --- Global mode ---
111
+ if (opts.global) {
112
+ await initGlobal({ force: opts.force, hooks: true });
113
+ return;
114
+ }
115
+
33
116
  // --- Adopt mode ---
34
117
  if (opts.adopt) {
35
118
  await adoptExisting(targetDir);
@@ -164,7 +247,7 @@ export async function initCommand(path, opts) {
164
247
  console.log(' .claude/CLAUDE.md — Project rules (review and customize)');
165
248
  console.log(' .claude/settings.json — Hook configuration');
166
249
  console.log(' .claude/hooks/ — 6 guards (file, path, glob, comment, sensitive, self-review)');
167
- console.log(' .claude/commands/ — /mf-plan, /mf-challenge, /mf-test, /mf-fix, /mf-review, /mf-commit');
250
+ console.log(' .claude/skills/ — /mf-plan, /mf-challenge, /mf-build, /mf-fix, /mf-review, /mf-commit');
168
251
  console.log(' scripts/build-test.sh — Universal test runner');
169
252
  console.log(' docs/WORKFLOW.md — Workflow reference');
170
253
  log.blank();
@@ -177,12 +260,84 @@ export async function initCommand(path, opts) {
177
260
  console.log(' 1. Review .claude/CLAUDE.md — ensure project info is correct');
178
261
  console.log(' 2. Write your first spec: docs/specs/<feature>.md');
179
262
  console.log(' 3. Generate test plan: /mf-plan docs/specs/<feature>.md');
180
- console.log(' 4. Start coding + testing: /mf-test');
263
+ console.log(' 4. Start coding + testing: /mf-build');
181
264
  log.blank();
182
265
 
183
266
  if (warnings > 0) {
184
267
  console.log(`⚠ ${warnings} warning(s) above — review before proceeding.`);
185
268
  }
269
+
270
+ // --- Global install prompt (first-time only) ---
271
+ if (!opts.global) {
272
+ const globalMeta = await readGlobalManifest();
273
+ if (globalMeta?.globalInstalled === undefined) {
274
+ await promptGlobalInstall(opts);
275
+ } else if (globalMeta?.globalInstalled === true) {
276
+ // Auto-upgrade global on init if previously installed
277
+ await initGlobal({ force: opts.force });
278
+ }
279
+ }
280
+ }
281
+
282
+ async function promptGlobalInstall(opts) {
283
+ log.blank();
284
+ console.log('─── Global Install ───');
285
+ console.log('');
286
+ console.log('Skills and hooks are installed per-project by default.');
287
+ console.log('You can install them globally so every project is covered without running init again.');
288
+ console.log('');
289
+ console.log(' ~/.claude/skills/ ← global skills (fallback when no per-project skills)');
290
+ console.log(' ~/.claude/hooks/ ← global hooks (active in all projects)');
291
+ console.log(' .claude/skills/ ← per-project skills (takes precedence over global)');
292
+ console.log(' .claude/hooks/ ← per-project hooks (takes precedence over global)');
293
+ console.log('');
294
+ console.log('To revert global hooks back to per-project later:');
295
+ console.log(' claude-devkit remove --global');
296
+ console.log(' then: claude-devkit init (in each project)');
297
+ console.log('');
298
+ console.log('RECOMMENDATION: Choose A if you work across many projects.');
299
+ console.log('');
300
+
301
+ const answer = await askGlobalInstall();
302
+
303
+ if (answer === 'skills+hooks') {
304
+ await initGlobal({ force: opts.force, hooks: true });
305
+ await trackProjectPath(process.cwd());
306
+ } else if (answer === 'skills') {
307
+ await initGlobal({ force: opts.force, hooks: false });
308
+ await trackProjectPath(process.cwd());
309
+ } else if (answer === 'no') {
310
+ await writeGlobalManifest({ globalInstalled: false, updatedAt: new Date().toISOString() });
311
+ log.info('Skipping global install. Run `claude-devkit init --global` anytime.');
312
+ }
313
+ // 'later' = don't write anything, prompt again next time
314
+ }
315
+
316
+ async function askGlobalInstall() {
317
+ const { createInterface } = await import('node:readline');
318
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
319
+ return new Promise((resolve) => {
320
+ console.log('A) Skills + Hooks globally (recommended)');
321
+ console.log('B) Skills only (hooks stay per-project)');
322
+ console.log('C) No — keep everything per-project');
323
+ console.log('D) Ask me next time');
324
+ console.log('');
325
+ rl.question('Choice [A/B/C/D]: ', (answer) => {
326
+ rl.close();
327
+ const a = answer.trim().toUpperCase();
328
+ if (a === 'A') resolve('skills+hooks');
329
+ else if (a === 'B') resolve('skills');
330
+ else if (a === 'C') resolve('no');
331
+ else resolve('later');
332
+ });
333
+ });
334
+ }
335
+
336
+ async function trackProjectPath(projectPath) {
337
+ const meta = await readGlobalManifest() || {};
338
+ const projects = new Set(meta.projects || []);
339
+ projects.add(projectPath);
340
+ await writeGlobalManifest({ ...meta, projects: [...projects] });
186
341
  }
187
342
 
188
343
  /**
@@ -1,8 +1,10 @@
1
1
  import { resolve, join } from 'node:path';
2
2
  import { unlink, rmdir, rm } from 'node:fs/promises';
3
3
  import { existsSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
4
5
  import { log } from '../lib/logger.js';
5
6
  import { readManifest } from '../lib/manifest.js';
7
+ import { removeGlobalHooksFromSettings } from '../lib/installer.js';
6
8
 
7
9
  const PRESERVE = [
8
10
  '.claude/CLAUDE.md',
@@ -12,7 +14,50 @@ const PRESERVE_DIRS = [
12
14
  'docs/',
13
15
  ];
14
16
 
15
- export async function removeCommand(path) {
17
+ export async function removeGlobal() {
18
+ log.info('Removing global claude-devkit install...');
19
+ log.blank();
20
+
21
+ // Remove ~/.claude/skills/
22
+ const globalSkillsDir = join(homedir(), '.claude', 'skills');
23
+ if (existsSync(globalSkillsDir)) {
24
+ await rm(globalSkillsDir, { recursive: true, force: true });
25
+ log.del('~/.claude/skills/');
26
+ } else {
27
+ log.skip('~/.claude/skills/ (not found)');
28
+ }
29
+
30
+ // Remove ~/.claude/hooks/
31
+ const globalHooksDir = join(homedir(), '.claude', 'hooks');
32
+ if (existsSync(globalHooksDir)) {
33
+ await rm(globalHooksDir, { recursive: true, force: true });
34
+ log.del('~/.claude/hooks/');
35
+ } else {
36
+ log.skip('~/.claude/hooks/ (not found)');
37
+ }
38
+
39
+ // Remove devkit hook entries from ~/.claude/settings.json
40
+ await removeGlobalHooksFromSettings();
41
+ log.del('hook entries from ~/.claude/settings.json');
42
+
43
+ // Remove global manifest
44
+ const globalManifest = join(homedir(), '.claude', '.devkit-manifest.json');
45
+ if (existsSync(globalManifest)) {
46
+ await unlink(globalManifest);
47
+ log.del('~/.claude/.devkit-manifest.json');
48
+ }
49
+
50
+ log.blank();
51
+ log.pass('Global install removed. Per-project installs are unaffected.');
52
+ log.info('Run `claude-devkit init` in each project to restore per-project hooks.');
53
+ }
54
+
55
+ export async function removeCommand(path, opts = {}) {
56
+ if (opts.global) {
57
+ await removeGlobal();
58
+ return;
59
+ }
60
+
16
61
  const targetDir = resolve(path);
17
62
  const manifest = await readManifest(targetDir);
18
63
 
@@ -56,7 +101,6 @@ export async function removeCommand(path) {
56
101
  // Clean up empty directories
57
102
  const dirsToClean = [
58
103
  '.claude/hooks',
59
- '.claude/commands',
60
104
  'scripts',
61
105
  ];
62
106
 
@@ -69,6 +113,13 @@ export async function removeCommand(path) {
69
113
  }
70
114
  }
71
115
 
116
+ // Skills are nested dirs — use recursive rm
117
+ const skillsDir = join(targetDir, '.claude/skills');
118
+ if (existsSync(skillsDir)) {
119
+ await rm(skillsDir, { recursive: true, force: true });
120
+ log.del('.claude/skills/');
121
+ }
122
+
72
123
  log.blank();
73
124
  log.pass('Removed. CLAUDE.md and docs/ preserved.');
74
125
  }
@@ -1,18 +1,92 @@
1
- import { resolve } from 'node:path';
1
+ import { resolve, dirname, join } from 'node:path';
2
2
  import { existsSync } from 'node:fs';
3
- import { copyFile as fsCopyFile, mkdir } from 'node:fs/promises';
4
- import { dirname } from 'node:path';
3
+ import { copyFile as fsCopyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
5
4
  import { readFileSync } from 'node:fs';
6
5
  import { fileURLToPath } from 'node:url';
6
+ import { homedir } from 'node:os';
7
7
  import { log } from '../lib/logger.js';
8
8
  import { hashFile } from '../lib/hasher.js';
9
9
  import { readManifest, writeManifest, setFileEntry, refreshCustomizationStatus } from '../lib/manifest.js';
10
- import { getAllFiles, getTemplateDir, setPermissions } from '../lib/installer.js';
10
+ import { getAllFiles, getTemplateDir, setPermissions, COMPONENTS, installSkillGlobal, getGlobalSkillsDir, installHookGlobal, getGlobalHooksDir, mergeGlobalSettings } from '../lib/installer.js';
11
+
12
+ const GLOBAL_MANIFEST = join(homedir(), '.claude', '.devkit-manifest.json');
13
+
14
+ async function readGlobalManifest() {
15
+ try { return JSON.parse(await readFile(GLOBAL_MANIFEST, 'utf-8')); } catch { return null; }
16
+ }
17
+ async function writeGlobalManifest(data) {
18
+ await mkdir(join(homedir(), '.claude'), { recursive: true });
19
+ await writeFile(GLOBAL_MANIFEST, JSON.stringify(data, null, 2) + '\n');
20
+ }
21
+
22
+ export async function upgradeGlobal({ force = false } = {}) {
23
+ const globalSkillsDir = getGlobalSkillsDir();
24
+ await mkdir(globalSkillsDir, { recursive: true });
25
+
26
+ log.blank();
27
+ console.log('--- Upgrading global skills ---');
28
+ let updated = 0; let skipped = 0; let identical = 0;
29
+
30
+ for (const relPath of COMPONENTS.skills) {
31
+ const result = await installSkillGlobal(relPath, globalSkillsDir, { force });
32
+ if (result === 'copied') updated++;
33
+ else if (result === 'identical') identical++;
34
+ else skipped++;
35
+ }
36
+
37
+ let skillParts = [`${updated} updated`, `${identical} unchanged`];
38
+ if (skipped > 0) skillParts.push(`${skipped} customized (use --force to overwrite)`);
39
+ log.pass(`Global skills: ${skillParts.join(', ')}`);
40
+
41
+ const meta = await readGlobalManifest() || {};
42
+
43
+ // Upgrade hooks if previously installed globally
44
+ if (meta.globalHooksInstalled) {
45
+ const globalHooksDir = getGlobalHooksDir();
46
+ await mkdir(globalHooksDir, { recursive: true });
47
+
48
+ log.blank();
49
+ console.log('--- Upgrading global hooks ---');
50
+ let hUpdated = 0; let hSkipped = 0; let hIdentical = 0;
51
+
52
+ for (const relPath of COMPONENTS.hooks) {
53
+ const result = await installHookGlobal(relPath, globalHooksDir, { force });
54
+ if (result === 'copied') hUpdated++;
55
+ else if (result === 'identical') hIdentical++;
56
+ else hSkipped++;
57
+ }
58
+
59
+ await mergeGlobalSettings(globalHooksDir);
60
+
61
+ let hookParts = [`${hUpdated} updated`, `${hIdentical} unchanged`];
62
+ if (hSkipped > 0) hookParts.push(`${hSkipped} customized (use --force to overwrite)`);
63
+ log.pass(`Global hooks: ${hookParts.join(', ')}`);
64
+ }
65
+
66
+ await writeGlobalManifest({ ...meta, globalInstalled: true, updatedAt: new Date().toISOString() });
67
+
68
+ // Warn about per-project skills that shadow global
69
+ const projects = meta.projects || [];
70
+ const projectsWithSkills = projects.filter((p) => existsSync(join(p, '.claude/skills')));
71
+ if (projectsWithSkills.length > 0) {
72
+ log.blank();
73
+ log.info(`Found per-project skills in ${projectsWithSkills.length} project(s):`);
74
+ for (const p of projectsWithSkills) log.info(` ${p}`);
75
+ log.info('Per-project skills take precedence over global. Remove them to use global instead.');
76
+ log.info('Run `claude-devkit remove <path>` in each project to remove per-project install.');
77
+ }
78
+ }
11
79
 
12
80
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
81
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8'));
14
82
 
15
83
  export async function upgradeCommand(path, opts) {
84
+ // --- Global mode ---
85
+ if (opts.global) {
86
+ await upgradeGlobal({ force: opts.force });
87
+ return;
88
+ }
89
+
16
90
  const targetDir = resolve(path);
17
91
  const manifest = await readManifest(targetDir);
18
92
 
@@ -122,4 +196,10 @@ export async function upgradeCommand(path, opts) {
122
196
  if (skippedCustomized > 0) {
123
197
  log.warn(`${skippedCustomized} customized file(s) skipped. Run with --force to overwrite.`);
124
198
  }
199
+
200
+ // --- Auto-upgrade global if previously installed ---
201
+ const globalMeta = await readGlobalManifest();
202
+ if (globalMeta?.globalInstalled === true) {
203
+ await upgradeGlobal({ force: opts.force });
204
+ }
125
205
  }
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { join, dirname, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { chmod } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
6
7
  import { log } from './logger.js';
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -19,13 +20,13 @@ export const COMPONENTS = {
19
20
  '.claude/hooks/self-review.sh',
20
21
  '.claude/hooks/sensitive-guard.sh',
21
22
  ],
22
- commands: [
23
- '.claude/commands/mf-plan.md',
24
- '.claude/commands/mf-challenge.md',
25
- '.claude/commands/mf-test.md',
26
- '.claude/commands/mf-fix.md',
27
- '.claude/commands/mf-review.md',
28
- '.claude/commands/mf-commit.md',
23
+ skills: [
24
+ '.claude/skills/mf-plan/SKILL.md',
25
+ '.claude/skills/mf-build/SKILL.md',
26
+ '.claude/skills/mf-challenge/SKILL.md',
27
+ '.claude/skills/mf-fix/SKILL.md',
28
+ '.claude/skills/mf-review/SKILL.md',
29
+ '.claude/skills/mf-commit/SKILL.md',
29
30
  ],
30
31
  config: [
31
32
  '.claude/settings.json',
@@ -69,7 +70,7 @@ export function getTemplateDir() {
69
70
 
70
71
  /**
71
72
  * Get all files for the given component list.
72
- * @param {string[]} components - e.g. ['hooks', 'commands']
73
+ * @param {string[]} components - e.g. ['hooks', 'skills']
73
74
  * @returns {string[]} relative file paths
74
75
  */
75
76
  export function getFilesForComponents(components) {
@@ -183,3 +184,176 @@ export async function verifySettingsJson(targetDir) {
183
184
  return false;
184
185
  }
185
186
  }
187
+
188
+ /**
189
+ * Global skills directory: ~/.claude/skills/
190
+ */
191
+ export function getGlobalSkillsDir() {
192
+ return join(homedir(), '.claude', 'skills');
193
+ }
194
+
195
+ /**
196
+ * Global hooks directory: ~/.claude/hooks/
197
+ */
198
+ export function getGlobalHooksDir() {
199
+ return join(homedir(), '.claude', 'hooks');
200
+ }
201
+
202
+ /**
203
+ * Copy a hook to the global ~/.claude/hooks/ directory.
204
+ * Strips the '.claude/hooks/' prefix so path-guard.sh lands at
205
+ * ~/.claude/hooks/path-guard.sh.
206
+ * @returns {string} 'copied' | 'skipped' | 'identical'
207
+ */
208
+ export async function installHookGlobal(hookRelPath, globalHooksDir, { force = false } = {}) {
209
+ const stripped = hookRelPath.replace(/^\.claude\/hooks\//, '');
210
+ const src = join(getTemplateDir(), hookRelPath);
211
+ const dst = join(globalHooksDir, stripped);
212
+
213
+ if (existsSync(dst) && !force) {
214
+ try {
215
+ const { hashFile } = await import('./hasher.js');
216
+ const srcHash = await hashFile(src);
217
+ const dstHash = await hashFile(dst);
218
+ if (srcHash === dstHash) {
219
+ log.same(`~/.claude/hooks/${stripped} (identical)`);
220
+ return 'identical';
221
+ }
222
+ log.skip(`~/.claude/hooks/${stripped} (customized — use --force to overwrite)`);
223
+ return 'skipped';
224
+ } catch { /* hash failed */ }
225
+ }
226
+
227
+ await mkdir(dirname(dst), { recursive: true });
228
+ await fsCopyFile(src, dst);
229
+ await chmod(dst, 0o755);
230
+ log.copy(`~/.claude/hooks/${stripped}`);
231
+ return 'copied';
232
+ }
233
+
234
+ /**
235
+ * Build hook entries for ~/.claude/settings.json pointing to globalHooksDir.
236
+ */
237
+ function buildGlobalHookEntries(globalHooksDir) {
238
+ // Normalize to forward slashes — bash on all platforms (WSL, Git Bash, macOS, Linux)
239
+ // requires forward slashes even when the host OS is Windows.
240
+ const dir = globalHooksDir.replace(/\\/g, '/');
241
+ const h = (file) => `"${dir}/${file}"`;
242
+ return {
243
+ PreToolUse: [
244
+ { matcher: 'Bash', hooks: [
245
+ { type: 'command', command: `bash ${h('path-guard.sh')}` },
246
+ { type: 'command', command: `bash ${h('sensitive-guard.sh')}` },
247
+ ]},
248
+ { matcher: 'Read|Write|Edit|MultiEdit|Grep', hooks: [
249
+ { type: 'command', command: `bash ${h('sensitive-guard.sh')}` },
250
+ ]},
251
+ { matcher: 'Edit|MultiEdit', hooks: [
252
+ { type: 'command', command: `node ${h('comment-guard.js')}` },
253
+ ]},
254
+ { matcher: 'Glob', hooks: [
255
+ { type: 'command', command: `node ${h('glob-guard.js')}` },
256
+ ]},
257
+ ],
258
+ PostToolUse: [
259
+ { matcher: 'Write|Edit|MultiEdit', hooks: [
260
+ { type: 'command', command: `node ${h('file-guard.js')}` },
261
+ ]},
262
+ ],
263
+ Stop: [
264
+ { matcher: '', hooks: [
265
+ { type: 'command', command: `bash ${h('self-review.sh')}` },
266
+ ]},
267
+ ],
268
+ };
269
+ }
270
+
271
+ function isDevkitHookCommand(command) {
272
+ return command.includes('/.claude/hooks/');
273
+ }
274
+
275
+ function stripDevkitHooks(existingHooks) {
276
+ if (!existingHooks || typeof existingHooks !== 'object') return {};
277
+ const result = {};
278
+ for (const [event, matchers] of Object.entries(existingHooks)) {
279
+ if (!Array.isArray(matchers)) continue;
280
+ const kept = [];
281
+ for (const group of matchers) {
282
+ const keptHooks = (group.hooks || []).filter((h) => !isDevkitHookCommand(h.command || ''));
283
+ if (keptHooks.length > 0) kept.push({ ...group, hooks: keptHooks });
284
+ }
285
+ if (kept.length > 0) result[event] = kept;
286
+ }
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * Merge devkit hook registrations into ~/.claude/settings.json.
292
+ * Preserves any existing non-devkit hooks the user may have.
293
+ */
294
+ export async function mergeGlobalSettings(globalHooksDir) {
295
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
296
+ let existing = {};
297
+ try {
298
+ existing = JSON.parse(await readFile(settingsPath, 'utf-8'));
299
+ } catch { /* file doesn't exist yet — start fresh */ }
300
+
301
+ // Remove old devkit entries (identified by /.claude/hooks/ in command path)
302
+ const cleanedHooks = stripDevkitHooks(existing.hooks);
303
+
304
+ // Append new devkit entries
305
+ const newEntries = buildGlobalHookEntries(globalHooksDir);
306
+ const mergedHooks = { ...cleanedHooks };
307
+ for (const [event, entries] of Object.entries(newEntries)) {
308
+ mergedHooks[event] = [...(mergedHooks[event] || []), ...entries];
309
+ }
310
+
311
+ await mkdir(dirname(settingsPath), { recursive: true });
312
+ await writeFile(settingsPath, JSON.stringify({ ...existing, hooks: mergedHooks }, null, 2) + '\n');
313
+ }
314
+
315
+ /**
316
+ * Remove devkit hook registrations from ~/.claude/settings.json.
317
+ * Leaves any non-devkit hooks untouched.
318
+ */
319
+ export async function removeGlobalHooksFromSettings() {
320
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
321
+ let existing = {};
322
+ try {
323
+ existing = JSON.parse(await readFile(settingsPath, 'utf-8'));
324
+ } catch { return; }
325
+
326
+ const cleanedHooks = stripDevkitHooks(existing.hooks || {});
327
+ await writeFile(settingsPath, JSON.stringify({ ...existing, hooks: cleanedHooks }, null, 2) + '\n');
328
+ }
329
+
330
+ /**
331
+ * Copy a skill to the global ~/.claude/skills/ directory.
332
+ * Strips the '.claude/skills/' prefix so mf-plan/SKILL.md lands at
333
+ * ~/.claude/skills/mf-plan/SKILL.md.
334
+ * @returns {string} 'copied' | 'skipped' | 'identical'
335
+ */
336
+ export async function installSkillGlobal(skillRelPath, globalSkillsDir, { force = false } = {}) {
337
+ const stripped = skillRelPath.replace(/^\.claude\/skills\//, '');
338
+ const src = join(getTemplateDir(), skillRelPath);
339
+ const dst = join(globalSkillsDir, stripped);
340
+
341
+ if (existsSync(dst) && !force) {
342
+ try {
343
+ const { hashFile } = await import('./hasher.js');
344
+ const srcHash = await hashFile(src);
345
+ const dstHash = await hashFile(dst);
346
+ if (srcHash === dstHash) {
347
+ log.same(`~/.claude/skills/${stripped} (identical)`);
348
+ return 'identical';
349
+ }
350
+ log.skip(`~/.claude/skills/${stripped} (customized — use --force to overwrite)`);
351
+ return 'skipped';
352
+ } catch { /* hash failed, treat as conflict */ }
353
+ }
354
+
355
+ await mkdir(dirname(dst), { recursive: true });
356
+ await fsCopyFile(src, dst);
357
+ log.copy(`~/.claude/skills/${stripped}`);
358
+ return 'copied';
359
+ }
@@ -36,7 +36,7 @@ export function createManifest(version, projectType, components) {
36
36
  installedAt: now,
37
37
  updatedAt: now,
38
38
  projectType: projectType || null,
39
- components: components || ['hooks', 'commands', 'scripts', 'docs'],
39
+ components: components || ['hooks', 'skills', 'scripts', 'docs'],
40
40
  files: {},
41
41
  };
42
42
  }
@@ -34,7 +34,7 @@ function isCommentLine(line) {
34
34
  if (trimmed.startsWith("#") && !trimmed.startsWith("#!")) return true;
35
35
  if (trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.endsWith("*/")) return true;
36
36
  if (trimmed.startsWith("<!--")) return true;
37
- if (trimmed === "pass") return true; // Python pass statement as placeholder
37
+ if (trimmed === "pass" || /^pass\s*#/.test(trimmed)) return true; // Python pass / pass # comment
38
38
  return false;
39
39
  }
40
40
 
@@ -33,6 +33,14 @@ const SCOPED_DIRS = [
33
33
  "Sources", "Tests", "cmd", "pkg", "internal",
34
34
  ];
35
35
 
36
+ // Allow project-specific scoped dirs via env var
37
+ // e.g. GLOB_GUARD_SCOPED_DIRS=Feature,Domain,Presentation
38
+ const extraDirs = (process.env.GLOB_GUARD_SCOPED_DIRS || "")
39
+ .split(",")
40
+ .map((d) => d.trim())
41
+ .filter(Boolean);
42
+ if (extraDirs.length > 0) SCOPED_DIRS.push(...extraDirs);
43
+
36
44
  function isBroadPattern(pattern) {
37
45
  if (!pattern) return false;
38
46
  return BROAD_PATTERNS.some((re) => re.test(pattern.trim()));
@@ -14,6 +14,10 @@
14
14
 
15
15
  set -euo pipefail
16
16
 
17
+ # Windows note: this hook requires bash (WSL or Git Bash).
18
+ # On Windows without bash, Claude Code will fail to run this hook and skip it silently.
19
+ # Install WSL or Git Bash and ensure `bash` is in PATH to activate protection.
20
+
17
21
  # ─── Read hook payload from stdin ───────────────────────────────────
18
22
 
19
23
  INPUT=$(cat)
@@ -41,40 +45,42 @@ COMMAND=$(extract_command "$INPUT") || exit 0
41
45
 
42
46
  # ─── Blocked directory patterns ─────────────────────────────────────
43
47
 
44
- # Use word boundaries (\b) and explicit path separators to avoid substring false positives
48
+ # Use explicit path separators to avoid substring false positives.
49
+ # [/\\] matches both forward slash (Unix/macOS) and backslash (Windows Git Bash).
45
50
  # e.g. "build/" should not match "rebuild/src" or "my-build-tool"
46
- BLOCKED="(^|[ /])node_modules(/|$| )"
51
+ SEP="[/\\\\]"
52
+ BLOCKED="(^|[ /\\\\])node_modules(${SEP}|$| )"
47
53
  BLOCKED+="|(__pycache__)"
48
- BLOCKED+="|\.git/(objects|refs)"
49
- BLOCKED+="|(^|[ /])dist/"
50
- BLOCKED+="|(^|[ /])build/"
51
- BLOCKED+="|\.next/"
52
- BLOCKED+="|(^|[ /])vendor(/|$| )"
53
- BLOCKED+="|(^|[ /])Pods(/|$| )"
54
- BLOCKED+="|\.build/"
54
+ BLOCKED+="|\.git${SEP}(objects|refs)"
55
+ BLOCKED+="|(^|[ /\\\\])dist${SEP}"
56
+ BLOCKED+="|(^|[ /\\\\])build${SEP}"
57
+ BLOCKED+="|\.next${SEP}"
58
+ BLOCKED+="|(^|[ /\\\\])vendor(${SEP}|$| )"
59
+ BLOCKED+="|(^|[ /\\\\])Pods(${SEP}|$| )"
60
+ BLOCKED+="|\.build${SEP}"
55
61
  BLOCKED+="|DerivedData"
56
- BLOCKED+="|\.gradle/"
57
- BLOCKED+="|(^|[ /])target/"
62
+ BLOCKED+="|\.gradle${SEP}"
63
+ BLOCKED+="|(^|[ /\\\\])target${SEP}"
58
64
  BLOCKED+="|\.nuget"
59
- BLOCKED+="|\.cache(/|$| )"
65
+ BLOCKED+="|\.cache(${SEP}|$| )"
60
66
  # Python
61
- BLOCKED+="|(^|[ /])\.venv/"
62
- BLOCKED+="|(^|[ /])venv/"
63
- BLOCKED+="|\.mypy_cache/"
64
- BLOCKED+="|\.pytest_cache/"
65
- BLOCKED+="|\.ruff_cache/"
66
- BLOCKED+="|\.egg-info(/|$| )"
67
+ BLOCKED+="|(^|[ /\\\\])\.venv${SEP}"
68
+ BLOCKED+="|(^|[ /\\\\])venv${SEP}"
69
+ BLOCKED+="|\.mypy_cache${SEP}"
70
+ BLOCKED+="|\.pytest_cache${SEP}"
71
+ BLOCKED+="|\.ruff_cache${SEP}"
72
+ BLOCKED+="|\.egg-info(${SEP}|$| )"
67
73
  # C# .NET (match .NET-specific subdirs to avoid false positives on generic bin/)
68
- BLOCKED+="|(^|[ /])bin/(Debug|Release|net|x64|x86)"
69
- BLOCKED+="|(^|[ /])obj/(Debug|Release|net)"
74
+ BLOCKED+="|(^|[ /\\\\])bin${SEP}(Debug|Release|net|x64|x86)"
75
+ BLOCKED+="|(^|[ /\\\\])obj${SEP}(Debug|Release|net)"
70
76
  # Node.js frameworks
71
- BLOCKED+="|\.nuxt/"
72
- BLOCKED+="|\.svelte-kit/"
73
- BLOCKED+="|\.parcel-cache/"
74
- BLOCKED+="|\.turbo/"
75
- BLOCKED+="|(^|[ /])out/(server|static|_next)"
77
+ BLOCKED+="|\.nuxt${SEP}"
78
+ BLOCKED+="|\.svelte-kit${SEP}"
79
+ BLOCKED+="|\.parcel-cache${SEP}"
80
+ BLOCKED+="|\.turbo${SEP}"
81
+ BLOCKED+="|(^|[ /\\\\])out${SEP}(server|static|_next)"
76
82
  # Ruby
77
- BLOCKED+="|\.bundle/"
83
+ BLOCKED+="|\.bundle${SEP}"
78
84
 
79
85
  # Append project-specific patterns from env
80
86
  if [[ -n "${PATH_GUARD_EXTRA:-}" ]]; then
@@ -8,6 +8,7 @@
8
8
  # SELF_REVIEW_ENABLED — set to "false" to disable (default: true)
9
9
 
10
10
  # No set -euo pipefail — this hook must NEVER fail
11
+ # Windows note: requires bash (WSL or Git Bash). Silently skipped on Windows native.
11
12
 
12
13
  # Check if disabled
13
14
  if [[ "${SELF_REVIEW_ENABLED:-true}" == "false" ]]; then
@@ -13,6 +13,10 @@
13
13
 
14
14
  set -euo pipefail
15
15
 
16
+ # Windows note: this hook requires bash (WSL or Git Bash).
17
+ # On Windows without bash, Claude Code will fail to run this hook and skip it silently.
18
+ # Install WSL or Git Bash and ensure `bash` is in PATH to activate protection.
19
+
16
20
  # ─── Read hook payload from stdin ───────────────────────────────────
17
21
 
18
22
  INPUT=$(cat)
@@ -117,7 +121,11 @@ check_agentignore() {
117
121
 
118
122
  # Simple line-by-line match (not full gitignore glob, but covers common cases)
119
123
  local relpath
120
- relpath=$(echo "$filepath" | sed "s|^$(pwd)/||") 2>/dev/null || relpath="$filepath"
124
+ # Normalize separators to forward slash before stripping prefix (handles Git Bash on Windows)
125
+ local normalized_fp normalized_pwd
126
+ normalized_fp=$(printf '%s' "$filepath" | tr '\\' '/')
127
+ normalized_pwd=$(pwd | tr '\\' '/')
128
+ relpath=$(printf '%s' "$normalized_fp" | sed "s|^${normalized_pwd}/||") 2>/dev/null || relpath="$filepath"
121
129
 
122
130
  while IFS= read -r pattern || [[ -n "$pattern" ]]; do
123
131
  # Skip comments and empty lines
@@ -214,14 +222,6 @@ if [[ -n "$COMMAND" ]]; then
214
222
  fi
215
223
  fi
216
224
 
217
- # ─── Check Grep pattern for sensitive file paths ───────────────────
218
-
219
- if [[ -n "$PATTERN" ]]; then
220
- if is_sensitive "$PATTERN"; then
221
- block_with_message "$PATTERN"
222
- fi
223
- fi
224
-
225
225
  # ─── All checks passed ─────────────────────────────────────────────
226
226
 
227
227
  exit 0
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: TDD delivery loop — write failing tests from spec, implement story by story, drive to GREEN
3
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
4
+ ---
1
5
  TDD delivery loop — write failing tests from spec AS, implement story by story, drive to GREEN.
2
6
 
3
7
  ## Phase 0: Build Context
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Adversarial review — spawn hostile reviewers to break the plan before coding
3
+ allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, Agent
4
+ ---
1
5
  Adversarial review — spawn hostile reviewers to break the plan before coding.
2
6
 
3
7
  ## Input
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Stage, scan secrets, generate conventional commit message
3
+ allowed-tools: Bash, AskUserQuestion
4
+ ---
1
5
  Stage, scan secrets, generate conventional commit message.
2
6
 
3
7
  ## Step 1 — Analyze (single compound command)
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Test-first bug fix — write failing test, fix code, verify green
3
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
4
+ ---
1
5
  Test-first bug fix — write failing test, fix code, verify green.
2
6
 
3
7
  Bug: $ARGUMENTS
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Generate spec with acceptance scenarios from description or existing spec
3
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Agent
4
+ ---
1
5
  Generate spec with acceptance scenarios from description or existing spec.
2
6
 
3
7
  ## Question Format
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Pre-merge code review — security, correctness, spec alignment
3
+ allowed-tools: Read, Bash, Glob, Grep
4
+ ---
1
5
  Pre-merge code review — security, correctness, spec alignment.
2
6
 
3
7
  ## Phase 0: Understand Intent