claude-devkit-cli 1.3.5 → 1.4.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/README.md +24 -15
- package/package.json +1 -1
- package/src/cli.js +6 -3
- package/src/commands/init.js +160 -5
- package/src/commands/remove.js +53 -2
- package/src/commands/upgrade.js +84 -4
- package/src/lib/installer.js +182 -8
- package/src/lib/manifest.js +1 -1
- package/templates/.claude/hooks/comment-guard.js +1 -1
- package/templates/.claude/hooks/glob-guard.js +8 -0
- package/templates/.claude/hooks/path-guard.sh +32 -26
- package/templates/.claude/hooks/self-review.sh +1 -0
- package/templates/.claude/hooks/sensitive-guard.sh +9 -9
- package/templates/.claude/{commands/mf-build.md → skills/mf-build/SKILL.md} +4 -0
- package/templates/.claude/{commands/mf-challenge.md → skills/mf-challenge/SKILL.md} +4 -0
- package/templates/.claude/{commands/mf-commit.md → skills/mf-commit/SKILL.md} +37 -3
- package/templates/.claude/{commands/mf-fix.md → skills/mf-fix/SKILL.md} +4 -0
- package/templates/.claude/{commands/mf-plan.md → skills/mf-plan/SKILL.md} +4 -0
- package/templates/.claude/{commands/mf-review.md → skills/mf-review/SKILL.md} +4 -0
package/README.md
CHANGED
|
@@ -109,7 +109,16 @@ cd my-project
|
|
|
109
109
|
claude-devkit init .
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
**Option C:
|
|
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,
|
|
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
|
-
│ └──
|
|
139
|
-
│ ├── mf-plan.md ← /mf-plan
|
|
140
|
-
│ ├── mf-challenge.md ← /mf-challenge
|
|
141
|
-
│ ├── mf-build.md ← /mf-build
|
|
142
|
-
│ ├── mf-fix.md ← /mf-fix
|
|
143
|
-
│ ├── mf-review.md ← /mf-review
|
|
144
|
-
│ └── mf-commit.md ← /mf-commit
|
|
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,
|
|
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
|
|
848
|
+
### Adding Custom Skills
|
|
840
849
|
|
|
841
|
-
Create new
|
|
850
|
+
Create new skills in `.claude/skills/<name>/SKILL.md`:
|
|
842
851
|
|
|
843
852
|
```markdown
|
|
844
|
-
# .claude/
|
|
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
|
|
952
|
-
A: Yes.
|
|
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
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('--
|
|
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
|
-
.
|
|
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);
|
package/src/commands/init.js
CHANGED
|
@@ -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/
|
|
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-
|
|
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
|
/**
|
package/src/commands/remove.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -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
|
}
|
package/src/lib/installer.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
'.claude/
|
|
24
|
-
'.claude/
|
|
25
|
-
'.claude/
|
|
26
|
-
'.claude/
|
|
27
|
-
'.claude/
|
|
28
|
-
'.claude/
|
|
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', '
|
|
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
|
+
}
|
package/src/lib/manifest.js
CHANGED
|
@@ -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', '
|
|
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
|
|
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
|
|
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
|
-
|
|
51
|
+
SEP="[/\\\\]"
|
|
52
|
+
BLOCKED="(^|[ /\\\\])node_modules(${SEP}|$| )"
|
|
47
53
|
BLOCKED+="|(__pycache__)"
|
|
48
|
-
BLOCKED+="|\.git
|
|
49
|
-
BLOCKED+="|(^|[
|
|
50
|
-
BLOCKED+="|(^|[
|
|
51
|
-
BLOCKED+="|\.next
|
|
52
|
-
BLOCKED+="|(^|[
|
|
53
|
-
BLOCKED+="|(^|[
|
|
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+="|(^|[
|
|
62
|
+
BLOCKED+="|\.gradle${SEP}"
|
|
63
|
+
BLOCKED+="|(^|[ /\\\\])target${SEP}"
|
|
58
64
|
BLOCKED+="|\.nuget"
|
|
59
|
-
BLOCKED+="|\.cache(
|
|
65
|
+
BLOCKED+="|\.cache(${SEP}|$| )"
|
|
60
66
|
# Python
|
|
61
|
-
BLOCKED+="|(^|[
|
|
62
|
-
BLOCKED+="|(^|[
|
|
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+="|(^|[
|
|
69
|
-
BLOCKED+="|(^|[
|
|
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+="|(^|[
|
|
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
|
-
|
|
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: 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)
|
|
@@ -102,7 +106,37 @@ Never stage: `.env`, credentials, build artifacts, generated files, binaries > 1
|
|
|
102
106
|
git commit -m "type(scope): description"
|
|
103
107
|
```
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Step 6 — Push?
|
|
112
|
+
|
|
113
|
+
Check if a remote exists:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git remote
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If no remote → skip this step entirely.
|
|
120
|
+
|
|
121
|
+
If remote exists, use `AskUserQuestion`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"questions": [
|
|
126
|
+
{
|
|
127
|
+
"question": "Commit successful. Push to remote now?",
|
|
128
|
+
"header": "Push",
|
|
129
|
+
"multiSelect": false,
|
|
130
|
+
"options": [
|
|
131
|
+
{"label": "Yes — push now (git push, or git push -u origin <branch> if no upstream)"},
|
|
132
|
+
{"label": "No — push later"}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If user chooses Yes → run `git push` (or `git push -u origin <branch>` if upstream not set).
|
|
106
140
|
|
|
107
141
|
---
|
|
108
142
|
|
|
@@ -112,7 +146,7 @@ git commit -m "type(scope): description"
|
|
|
112
146
|
staged: N files (+X/-Y lines)
|
|
113
147
|
checks: secrets ✓ | debug ✓
|
|
114
148
|
commit: abc1234 type(scope): description
|
|
115
|
-
pushed: no
|
|
149
|
+
pushed: yes → origin/<branch> (or "no")
|
|
116
150
|
```
|
|
117
151
|
|
|
118
152
|
Keep under 5 lines. No explanations.
|
|
@@ -120,5 +154,5 @@ Keep under 5 lines. No explanations.
|
|
|
120
154
|
## Rules
|
|
121
155
|
1. **Specific files, not `git add -A`.** Stage intentionally.
|
|
122
156
|
2. **Secrets = hard block.** No exceptions.
|
|
123
|
-
3. **
|
|
157
|
+
3. **Ask before pushing.** Push only if user confirms in Step 6.
|
|
124
158
|
4. **One concern per commit.** Mixed features → suggest separate commits.
|
|
@@ -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
|