claude-devkit-cli 1.3.3 → 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 +40 -31
- 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/CLAUDE.md +2 -2
- 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-test.md → skills/mf-build/SKILL.md} +23 -3
- package/templates/.claude/{commands/mf-challenge.md → skills/mf-challenge/SKILL.md} +42 -27
- package/templates/.claude/{commands/mf-commit.md → skills/mf-commit/SKILL.md} +39 -8
- package/templates/.claude/{commands/mf-fix.md → skills/mf-fix/SKILL.md} +22 -1
- package/templates/.claude/{commands/mf-plan.md → skills/mf-plan/SKILL.md} +59 -48
- package/templates/.claude/{commands/mf-review.md → skills/mf-review/SKILL.md} +4 -0
- package/templates/docs/WORKFLOW.md +5 -5
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ claude
|
|
|
66
66
|
/mf-plan "describe your feature here"
|
|
67
67
|
|
|
68
68
|
# 4. Write code, then test
|
|
69
|
-
/mf-
|
|
69
|
+
/mf-build
|
|
70
70
|
|
|
71
71
|
# 5. Review before merging
|
|
72
72
|
/mf-review
|
|
@@ -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-
|
|
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
|
|
|
@@ -202,7 +211,7 @@ This removes hooks, commands, settings, and build-test.sh. It preserves `CLAUDE.
|
|
|
202
211
|
→ Generates spec with acceptance scenarios at docs/specs/<feature>/<feature>.md.
|
|
203
212
|
|
|
204
213
|
2. Implement code in chunks.
|
|
205
|
-
After each chunk: /mf-
|
|
214
|
+
After each chunk: /mf-build
|
|
206
215
|
Repeat until green.
|
|
207
216
|
|
|
208
217
|
3. /mf-review (before merge)
|
|
@@ -225,7 +234,7 @@ This removes hooks, commands, settings, and build-test.sh. It preserves `CLAUDE.
|
|
|
225
234
|
Do NOT manually edit the spec before running /mf-plan.
|
|
226
235
|
|
|
227
236
|
2. Implement the code change.
|
|
228
|
-
/mf-
|
|
237
|
+
/mf-build
|
|
229
238
|
Fix until green.
|
|
230
239
|
|
|
231
240
|
3. /mf-review → /mf-commit
|
|
@@ -392,21 +401,21 @@ docs/specs/<feature>/
|
|
|
392
401
|
|
|
393
402
|
**Token cost:** 15-30k (uses parallel subagents, doesn't bloat main context)
|
|
394
403
|
|
|
395
|
-
### /mf-
|
|
404
|
+
### /mf-build — TDD Delivery Loop
|
|
396
405
|
|
|
397
406
|
**Usage:**
|
|
398
407
|
```
|
|
399
|
-
/mf-
|
|
400
|
-
/mf-
|
|
401
|
-
/mf-
|
|
408
|
+
/mf-build # build all changes vs base branch
|
|
409
|
+
/mf-build src/api/users.ts # build specific file
|
|
410
|
+
/mf-build "user authentication" # build specific feature
|
|
402
411
|
```
|
|
403
412
|
|
|
404
413
|
**How it works:**
|
|
405
414
|
|
|
406
415
|
1. **Phase 0: Build Context** — Finds changed files vs base branch, reads the spec (acceptance scenarios in `## Stories` section are the roadmap), reads existing tests for patterns, fixtures, and naming conventions. Doesn't duplicate what already exists.
|
|
407
|
-
2. **Phase 1: Write Tests** — Creates
|
|
416
|
+
2. **Phase 1: Write Failing Tests** — Creates tests from acceptance scenarios (RED). Each test covers one AS, is independent, deterministic (no random, no time-dependent, no external calls), and has a clear name.
|
|
408
417
|
3. **Phase 2: Compile First** — Runs typecheck/compile before executing tests. Catches syntax errors early.
|
|
409
|
-
4. **Phase 3: Run
|
|
418
|
+
4. **Phase 3: Implement + Run** — Implements story code, executes tests, drives to GREEN story by story.
|
|
410
419
|
5. **Phase 4: Fix Loop** — If tests fail, fixes **test code only** (max 3 attempts, then hard stop and report). If tests expect X but code does Y, asks you whether to fix production code or adjust the test.
|
|
411
420
|
6. **Phase 5: Report** — Summary with test counts, results, coverage, and files touched.
|
|
412
421
|
|
|
@@ -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
|
|
@@ -858,7 +867,7 @@ Then use: `/deploy staging`
|
|
|
858
867
|
|
|
859
868
|
| Activity | Tokens | Frequency |
|
|
860
869
|
|----------|--------|-----------|
|
|
861
|
-
| `/mf-
|
|
870
|
+
| `/mf-build` (incremental, 1-3 files) | 5–10k | Every code chunk |
|
|
862
871
|
| `/mf-fix` (single bug) | 3–5k | As needed |
|
|
863
872
|
| `/mf-commit` | 2–4k | Every commit |
|
|
864
873
|
| `/mf-review` (diff-based) | 10–20k | Before merge |
|
|
@@ -868,9 +877,9 @@ Then use: `/deploy staging`
|
|
|
868
877
|
|
|
869
878
|
### Minimizing Token Usage
|
|
870
879
|
|
|
871
|
-
- **Test incrementally.** `/mf-
|
|
872
|
-
- **Use filters.** `/mf-
|
|
873
|
-
- **Skip `/mf-plan` for tiny changes.** Under 5 lines with no behavior change? Just `/mf-
|
|
880
|
+
- **Test incrementally.** `/mf-build` after each small chunk uses 5-10k. Waiting until everything is done then running `/mf-build` on a large diff uses 50k+.
|
|
881
|
+
- **Use filters.** `/mf-build src/auth/login.ts` is cheaper than `/mf-build` on the whole project.
|
|
882
|
+
- **Skip `/mf-plan` for tiny changes.** Under 5 lines with no behavior change? Just `/mf-build` and `/mf-commit`.
|
|
874
883
|
- **Use `/mf-review` only before merge.** Not after every commit.
|
|
875
884
|
|
|
876
885
|
---
|
|
@@ -898,7 +907,7 @@ Then use: `/deploy staging`
|
|
|
898
907
|
|
|
899
908
|
### Wrong base branch
|
|
900
909
|
|
|
901
|
-
**Symptom:** `/mf-
|
|
910
|
+
**Symptom:** `/mf-build` or `/mf-review` compares against wrong branch.
|
|
902
911
|
|
|
903
912
|
**Check:**
|
|
904
913
|
```bash
|
|
@@ -928,13 +937,13 @@ export FILE_GUARD_EXCLUDE="*.generated.swift,*.pb.go,*.min.js,*.snap"
|
|
|
928
937
|
## 12. FAQ
|
|
929
938
|
|
|
930
939
|
**Q: Do I need specs for every tiny change?**
|
|
931
|
-
A: No. Changes under 5 lines with no behavior change can skip the spec. Just `/mf-
|
|
940
|
+
A: No. Changes under 5 lines with no behavior change can skip the spec. Just `/mf-build` and `/mf-commit`. The spec-first rule is for meaningful behavior changes.
|
|
932
941
|
|
|
933
942
|
**Q: Can I use mocks in tests?**
|
|
934
943
|
A: Only for external services you can't run locally (third-party APIs, email services). Never mock your own code or database just to make tests pass faster.
|
|
935
944
|
|
|
936
945
|
**Q: What if Claude writes a test that tests the wrong thing?**
|
|
937
|
-
A: This usually means the spec is ambiguous. Clarify the spec first, then re-run `/mf-
|
|
946
|
+
A: This usually means the spec is ambiguous. Clarify the spec first, then re-run `/mf-build`. Good specs produce good tests.
|
|
938
947
|
|
|
939
948
|
**Q: Can I use this with other AI coding tools?**
|
|
940
949
|
A: The commands and hooks are Claude Code-specific. The specs, workflow, and `build-test.sh` work with any tool or manual workflow.
|
|
@@ -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
|
}
|