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 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-test
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: 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-test.md ← /mf-test 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
 
@@ -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-test
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-test
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-testWrite + Run Tests
404
+ ### /mf-buildTDD Delivery Loop
396
405
 
397
406
  **Usage:**
398
407
  ```
399
- /mf-test # test all changes vs base branch
400
- /mf-test src/api/users.ts # test specific file
401
- /mf-test "user authentication" # test specific feature
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 or updates tests based on acceptance scenarios. Each test covers one concept, is independent, deterministic (no random, no time-dependent, no external calls), and has a clear name.
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 Tests** — Executes the test suite.
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 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
@@ -858,7 +867,7 @@ Then use: `/deploy staging`
858
867
 
859
868
  | Activity | Tokens | Frequency |
860
869
  |----------|--------|-----------|
861
- | `/mf-test` (incremental, 1-3 files) | 5–10k | Every code chunk |
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-test` after each small chunk uses 5-10k. Waiting until everything is done then running `/mf-test` on a large diff uses 50k+.
872
- - **Use filters.** `/mf-test src/auth/login.ts` is cheaper than `/mf-test` on the whole project.
873
- - **Skip `/mf-plan` for tiny changes.** Under 5 lines with no behavior change? Just `/mf-test` and `/mf-commit`.
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-test` or `/mf-review` compares against wrong branch.
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-test` and `/mf-commit`. The spec-first rule is for meaningful behavior changes.
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-test`. Good specs produce good tests.
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 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.3",
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
  }