claude-devkit-cli 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -37,15 +37,20 @@ export async function initGlobal({ force = false, hooks = false } = {}) {
|
|
|
37
37
|
const globalSkillsDir = getGlobalSkillsDir();
|
|
38
38
|
await mkdir(globalSkillsDir, { recursive: true });
|
|
39
39
|
|
|
40
|
+
const existing = await readGlobalManifest() || {};
|
|
41
|
+
const globalFiles = existing.files || {};
|
|
42
|
+
const updatedFiles = { ...globalFiles };
|
|
43
|
+
|
|
40
44
|
log.blank();
|
|
41
45
|
console.log('--- Installing global skills ---');
|
|
42
46
|
|
|
43
47
|
let copied = 0; let skipped = 0; let identical = 0;
|
|
44
48
|
for (const relPath of COMPONENTS.skills) {
|
|
45
|
-
const result = await installSkillGlobal(relPath, globalSkillsDir, { force });
|
|
49
|
+
const { result, kitHash } = await installSkillGlobal(relPath, globalSkillsDir, { force, globalFiles });
|
|
46
50
|
if (result === 'copied') copied++;
|
|
47
51
|
else if (result === 'identical') identical++;
|
|
48
52
|
else skipped++;
|
|
53
|
+
if (result !== 'skipped') updatedFiles[relPath] = { kitHash };
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const parts = [`${copied} copied`];
|
|
@@ -55,32 +60,36 @@ export async function initGlobal({ force = false, hooks = false } = {}) {
|
|
|
55
60
|
log.info('Skills available in all projects via ~/.claude/skills/');
|
|
56
61
|
|
|
57
62
|
if (hooks) {
|
|
58
|
-
await initGlobalHooks({ force });
|
|
63
|
+
await initGlobalHooks({ force, _globalFiles: updatedFiles, _skipManifestWrite: true });
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
// Write global manifest
|
|
62
|
-
const existing = await readGlobalManifest() || {};
|
|
63
66
|
await writeGlobalManifest({
|
|
64
67
|
...existing,
|
|
65
68
|
globalInstalled: true,
|
|
66
69
|
globalHooksInstalled: hooks || existing.globalHooksInstalled || false,
|
|
70
|
+
files: updatedFiles,
|
|
67
71
|
updatedAt: new Date().toISOString(),
|
|
68
72
|
});
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
export async function initGlobalHooks({ force = false } = {}) {
|
|
75
|
+
export async function initGlobalHooks({ force = false, _globalFiles, _skipManifestWrite = false } = {}) {
|
|
72
76
|
const globalHooksDir = getGlobalHooksDir();
|
|
73
77
|
await mkdir(globalHooksDir, { recursive: true });
|
|
74
78
|
|
|
79
|
+
const existing = _skipManifestWrite ? null : (await readGlobalManifest() || {});
|
|
80
|
+
const globalFiles = _globalFiles || existing?.files || {};
|
|
81
|
+
const updatedFiles = { ...globalFiles };
|
|
82
|
+
|
|
75
83
|
log.blank();
|
|
76
84
|
console.log('--- Installing global hooks ---');
|
|
77
85
|
|
|
78
86
|
let copied = 0; let skipped = 0; let identical = 0;
|
|
79
87
|
for (const relPath of COMPONENTS.hooks) {
|
|
80
|
-
const result = await installHookGlobal(relPath, globalHooksDir, { force });
|
|
88
|
+
const { result, kitHash } = await installHookGlobal(relPath, globalHooksDir, { force, globalFiles });
|
|
81
89
|
if (result === 'copied') copied++;
|
|
82
90
|
else if (result === 'identical') identical++;
|
|
83
91
|
else skipped++;
|
|
92
|
+
if (result !== 'skipped') updatedFiles[relPath] = { kitHash };
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
await mergeGlobalSettings(globalHooksDir);
|
|
@@ -90,6 +99,15 @@ export async function initGlobalHooks({ force = false } = {}) {
|
|
|
90
99
|
if (skipped > 0) parts.push(`${skipped} customized (use --force to overwrite)`);
|
|
91
100
|
log.pass(`Global hooks: ${parts.join(', ')}`);
|
|
92
101
|
log.info('Hooks registered in ~/.claude/settings.json — active in all projects');
|
|
102
|
+
|
|
103
|
+
if (!_skipManifestWrite) {
|
|
104
|
+
await writeGlobalManifest({
|
|
105
|
+
...existing,
|
|
106
|
+
globalHooksInstalled: true,
|
|
107
|
+
files: updatedFiles,
|
|
108
|
+
updatedAt: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
package/src/commands/upgrade.js
CHANGED
|
@@ -23,23 +23,26 @@ export async function upgradeGlobal({ force = false } = {}) {
|
|
|
23
23
|
const globalSkillsDir = getGlobalSkillsDir();
|
|
24
24
|
await mkdir(globalSkillsDir, { recursive: true });
|
|
25
25
|
|
|
26
|
+
const meta = await readGlobalManifest() || {};
|
|
27
|
+
const globalFiles = meta.files || {};
|
|
28
|
+
const updatedFiles = { ...globalFiles };
|
|
29
|
+
|
|
26
30
|
log.blank();
|
|
27
31
|
console.log('--- Upgrading global skills ---');
|
|
28
32
|
let updated = 0; let skipped = 0; let identical = 0;
|
|
29
33
|
|
|
30
34
|
for (const relPath of COMPONENTS.skills) {
|
|
31
|
-
const result = await installSkillGlobal(relPath, globalSkillsDir, { force });
|
|
35
|
+
const { result, kitHash } = await installSkillGlobal(relPath, globalSkillsDir, { force, globalFiles });
|
|
32
36
|
if (result === 'copied') updated++;
|
|
33
37
|
else if (result === 'identical') identical++;
|
|
34
38
|
else skipped++;
|
|
39
|
+
if (result !== 'skipped') updatedFiles[relPath] = { kitHash };
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
let skillParts = [`${updated} updated`, `${identical} unchanged`];
|
|
38
43
|
if (skipped > 0) skillParts.push(`${skipped} customized (use --force to overwrite)`);
|
|
39
44
|
log.pass(`Global skills: ${skillParts.join(', ')}`);
|
|
40
45
|
|
|
41
|
-
const meta = await readGlobalManifest() || {};
|
|
42
|
-
|
|
43
46
|
// Upgrade hooks if previously installed globally
|
|
44
47
|
if (meta.globalHooksInstalled) {
|
|
45
48
|
const globalHooksDir = getGlobalHooksDir();
|
|
@@ -50,10 +53,11 @@ export async function upgradeGlobal({ force = false } = {}) {
|
|
|
50
53
|
let hUpdated = 0; let hSkipped = 0; let hIdentical = 0;
|
|
51
54
|
|
|
52
55
|
for (const relPath of COMPONENTS.hooks) {
|
|
53
|
-
const result = await installHookGlobal(relPath, globalHooksDir, { force });
|
|
56
|
+
const { result, kitHash } = await installHookGlobal(relPath, globalHooksDir, { force, globalFiles });
|
|
54
57
|
if (result === 'copied') hUpdated++;
|
|
55
58
|
else if (result === 'identical') hIdentical++;
|
|
56
59
|
else hSkipped++;
|
|
60
|
+
if (result !== 'skipped') updatedFiles[relPath] = { kitHash };
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
await mergeGlobalSettings(globalHooksDir);
|
|
@@ -63,7 +67,7 @@ export async function upgradeGlobal({ force = false } = {}) {
|
|
|
63
67
|
log.pass(`Global hooks: ${hookParts.join(', ')}`);
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
await writeGlobalManifest({ ...meta, globalInstalled: true, updatedAt: new Date().toISOString() });
|
|
70
|
+
await writeGlobalManifest({ ...meta, globalInstalled: true, files: updatedFiles, updatedAt: new Date().toISOString() });
|
|
67
71
|
|
|
68
72
|
// Warn about per-project skills that shadow global
|
|
69
73
|
const projects = meta.projects || [];
|
package/src/lib/installer.js
CHANGED
|
@@ -203,24 +203,31 @@ export function getGlobalHooksDir() {
|
|
|
203
203
|
* Copy a hook to the global ~/.claude/hooks/ directory.
|
|
204
204
|
* Strips the '.claude/hooks/' prefix so path-guard.sh lands at
|
|
205
205
|
* ~/.claude/hooks/path-guard.sh.
|
|
206
|
-
* @
|
|
206
|
+
* @param {object} [opts.globalFiles] - files section from global manifest, used to detect true customization
|
|
207
|
+
* @returns {{ result: 'copied'|'skipped'|'identical', kitHash: string }}
|
|
207
208
|
*/
|
|
208
|
-
export async function installHookGlobal(hookRelPath, globalHooksDir, { force = false } = {}) {
|
|
209
|
+
export async function installHookGlobal(hookRelPath, globalHooksDir, { force = false, globalFiles = {} } = {}) {
|
|
209
210
|
const stripped = hookRelPath.replace(/^\.claude\/hooks\//, '');
|
|
210
211
|
const src = join(getTemplateDir(), hookRelPath);
|
|
211
212
|
const dst = join(globalHooksDir, stripped);
|
|
212
213
|
|
|
214
|
+
const { hashFile } = await import('./hasher.js');
|
|
215
|
+
const srcHash = await hashFile(src);
|
|
216
|
+
|
|
213
217
|
if (existsSync(dst) && !force) {
|
|
214
218
|
try {
|
|
215
|
-
const { hashFile } = await import('./hasher.js');
|
|
216
|
-
const srcHash = await hashFile(src);
|
|
217
219
|
const dstHash = await hashFile(dst);
|
|
218
220
|
if (srcHash === dstHash) {
|
|
219
221
|
log.same(`~/.claude/hooks/${stripped} (identical)`);
|
|
220
|
-
return 'identical';
|
|
222
|
+
return { result: 'identical', kitHash: srcHash };
|
|
223
|
+
}
|
|
224
|
+
const savedKitHash = globalFiles[hookRelPath]?.kitHash;
|
|
225
|
+
if (savedKitHash && dstHash === savedKitHash) {
|
|
226
|
+
// fall through to copy
|
|
227
|
+
} else {
|
|
228
|
+
log.skip(`~/.claude/hooks/${stripped} (customized — use --force to overwrite)`);
|
|
229
|
+
return { result: 'skipped', kitHash: srcHash };
|
|
221
230
|
}
|
|
222
|
-
log.skip(`~/.claude/hooks/${stripped} (customized — use --force to overwrite)`);
|
|
223
|
-
return 'skipped';
|
|
224
231
|
} catch { /* hash failed */ }
|
|
225
232
|
}
|
|
226
233
|
|
|
@@ -228,7 +235,7 @@ export async function installHookGlobal(hookRelPath, globalHooksDir, { force = f
|
|
|
228
235
|
await fsCopyFile(src, dst);
|
|
229
236
|
await chmod(dst, 0o755);
|
|
230
237
|
log.copy(`~/.claude/hooks/${stripped}`);
|
|
231
|
-
return 'copied';
|
|
238
|
+
return { result: 'copied', kitHash: srcHash };
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
/**
|
|
@@ -331,29 +338,38 @@ export async function removeGlobalHooksFromSettings() {
|
|
|
331
338
|
* Copy a skill to the global ~/.claude/skills/ directory.
|
|
332
339
|
* Strips the '.claude/skills/' prefix so mf-plan/SKILL.md lands at
|
|
333
340
|
* ~/.claude/skills/mf-plan/SKILL.md.
|
|
334
|
-
* @
|
|
341
|
+
* @param {object} [opts.globalFiles] - files section from global manifest, used to detect true customization
|
|
342
|
+
* @returns {{ result: 'copied'|'skipped'|'identical', kitHash: string }}
|
|
335
343
|
*/
|
|
336
|
-
export async function installSkillGlobal(skillRelPath, globalSkillsDir, { force = false } = {}) {
|
|
344
|
+
export async function installSkillGlobal(skillRelPath, globalSkillsDir, { force = false, globalFiles = {} } = {}) {
|
|
337
345
|
const stripped = skillRelPath.replace(/^\.claude\/skills\//, '');
|
|
338
346
|
const src = join(getTemplateDir(), skillRelPath);
|
|
339
347
|
const dst = join(globalSkillsDir, stripped);
|
|
340
348
|
|
|
349
|
+
const { hashFile } = await import('./hasher.js');
|
|
350
|
+
const srcHash = await hashFile(src);
|
|
351
|
+
|
|
341
352
|
if (existsSync(dst) && !force) {
|
|
342
353
|
try {
|
|
343
|
-
const { hashFile } = await import('./hasher.js');
|
|
344
|
-
const srcHash = await hashFile(src);
|
|
345
354
|
const dstHash = await hashFile(dst);
|
|
346
355
|
if (srcHash === dstHash) {
|
|
347
356
|
log.same(`~/.claude/skills/${stripped} (identical)`);
|
|
348
|
-
return 'identical';
|
|
357
|
+
return { result: 'identical', kitHash: srcHash };
|
|
358
|
+
}
|
|
359
|
+
// If the installed file still matches the kitHash we saved at last install,
|
|
360
|
+
// the user hasn't touched it — the kit just changed. Safe to update.
|
|
361
|
+
const savedKitHash = globalFiles[skillRelPath]?.kitHash;
|
|
362
|
+
if (savedKitHash && dstHash === savedKitHash) {
|
|
363
|
+
// fall through to copy
|
|
364
|
+
} else {
|
|
365
|
+
log.skip(`~/.claude/skills/${stripped} (customized — use --force to overwrite)`);
|
|
366
|
+
return { result: 'skipped', kitHash: srcHash };
|
|
349
367
|
}
|
|
350
|
-
log.skip(`~/.claude/skills/${stripped} (customized — use --force to overwrite)`);
|
|
351
|
-
return 'skipped';
|
|
352
368
|
} catch { /* hash failed, treat as conflict */ }
|
|
353
369
|
}
|
|
354
370
|
|
|
355
371
|
await mkdir(dirname(dst), { recursive: true });
|
|
356
372
|
await fsCopyFile(src, dst);
|
|
357
373
|
log.copy(`~/.claude/skills/${stripped}`);
|
|
358
|
-
return 'copied';
|
|
374
|
+
return { result: 'copied', kitHash: srcHash };
|
|
359
375
|
}
|
|
@@ -106,7 +106,37 @@ Never stage: `.env`, credentials, build artifacts, generated files, binaries > 1
|
|
|
106
106
|
git commit -m "type(scope): description"
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
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).
|
|
110
140
|
|
|
111
141
|
---
|
|
112
142
|
|
|
@@ -116,7 +146,7 @@ git commit -m "type(scope): description"
|
|
|
116
146
|
staged: N files (+X/-Y lines)
|
|
117
147
|
checks: secrets ✓ | debug ✓
|
|
118
148
|
commit: abc1234 type(scope): description
|
|
119
|
-
pushed: no
|
|
149
|
+
pushed: yes → origin/<branch> (or "no")
|
|
120
150
|
```
|
|
121
151
|
|
|
122
152
|
Keep under 5 lines. No explanations.
|
|
@@ -124,5 +154,5 @@ Keep under 5 lines. No explanations.
|
|
|
124
154
|
## Rules
|
|
125
155
|
1. **Specific files, not `git add -A`.** Stage intentionally.
|
|
126
156
|
2. **Secrets = hard block.** No exceptions.
|
|
127
|
-
3. **
|
|
157
|
+
3. **Ask before pushing.** Push only if user confirms in Step 6.
|
|
128
158
|
4. **One concern per commit.** Mixed features → suggest separate commits.
|