specpipe 1.0.2 → 1.0.3
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specpipe",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Spec-first development toolkit for agentic AI coding agents — installs skills + guardrails for Claude Code, Codex, Cursor, Antigravity, and more.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"specpipe": "./bin/devkit.js",
|
|
@@ -9,7 +9,7 @@ import { hashContent } from '../lib/hasher.js';
|
|
|
9
9
|
import {
|
|
10
10
|
COMPONENTS,
|
|
11
11
|
fillTemplate, installAgentSkills, installAgentRules, installAgentHooks,
|
|
12
|
-
resolveSkills,
|
|
12
|
+
resolveSkills, pruneOrphans,
|
|
13
13
|
} from '../lib/installer.js';
|
|
14
14
|
import { resolveAgents, AGENTS, agentHasHooks } from '../lib/agents.js';
|
|
15
15
|
import { resolveHooks } from '../lib/hooks.js';
|
|
@@ -102,6 +102,14 @@ export async function initMultiAgent(targetDir, opts, warnings = 0) {
|
|
|
102
102
|
setFileEntry(manifest, relPath, d.kitHash, installedHash, { agent: d.agent, templateRel: d.templateRel });
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// Migration: prune predecessor files (mf-* / ap-*, renamed hooks) a prior manifest
|
|
106
|
+
// tracked but this install no longer wants — only manifest-tracked paths, so a
|
|
107
|
+
// user's own files are never touched.
|
|
108
|
+
if (existing?.files) {
|
|
109
|
+
const n = await pruneOrphans(targetDir, existing.files, new Set(Object.keys(manifest.files)));
|
|
110
|
+
if (n) log.info(`Migrated: removed ${n} superseded file(s) from a previous version.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
105
113
|
await writeManifest(targetDir, manifest);
|
|
106
114
|
|
|
107
115
|
// Summary
|
|
@@ -68,8 +68,9 @@ export async function initGlobal({ agents = ['claude'], skills = null, hookSelec
|
|
|
68
68
|
// Global hooks are Claude-only (its native enforcement engine).
|
|
69
69
|
const wantHooks = hooks && agents.includes('claude');
|
|
70
70
|
if (wantHooks) {
|
|
71
|
-
const hookKeys = await initGlobalHooks({ force, hooks: hookSelection, _globalFiles: updatedFiles, _skipManifestWrite: true });
|
|
71
|
+
const { keys: hookKeys, entries: hookEntries } = await initGlobalHooks({ force, hooks: hookSelection, _globalFiles: updatedFiles, _skipManifestWrite: true });
|
|
72
72
|
for (const k of hookKeys) installedKeys.add(k);
|
|
73
|
+
Object.assign(updatedFiles, hookEntries); // persist hook kitHashes in the global manifest
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// Prune orphans: files from a previous global install that are no longer desired
|
|
@@ -101,6 +102,18 @@ export async function initGlobal({ agents = ['claude'], skills = null, hookSelec
|
|
|
101
102
|
}
|
|
102
103
|
if (pruned) log.info(`Pruned ${pruned} stale global file(s).`);
|
|
103
104
|
|
|
105
|
+
// Legacy migration: older claude-devkit installs left ~/.claude/scripts/build-test.sh
|
|
106
|
+
// (no longer in the kit, untracked by the manifest). Sweep it up on install too —
|
|
107
|
+
// not just on remove — so upgrading from the old tool leaves nothing behind.
|
|
108
|
+
if (wantHooks) {
|
|
109
|
+
const legacyScript = join(homedir(), '.claude', 'scripts', 'build-test.sh');
|
|
110
|
+
try {
|
|
111
|
+
await unlink(legacyScript);
|
|
112
|
+
log.del('~/.claude/scripts/build-test.sh (legacy)');
|
|
113
|
+
try { await rmdir(join(homedir(), '.claude', 'scripts')); } catch { /* keep if other scripts */ }
|
|
114
|
+
} catch { /* not present — fine */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
await writeGlobalManifest({
|
|
105
118
|
...existing,
|
|
106
119
|
globalInstalled: installedAgents.has('claude') || existing.globalInstalled || false,
|
|
@@ -121,6 +134,9 @@ export async function initGlobalHooks({ force = false, hooks = null, _globalFile
|
|
|
121
134
|
const globalFiles = _globalFiles || existing?.files || {};
|
|
122
135
|
const updatedFiles = { ...globalFiles };
|
|
123
136
|
const keys = []; // home-relative keys installed this run (for the caller's orphan-prune)
|
|
137
|
+
const entries = {}; // kitHash entries written this run — the caller persists these so
|
|
138
|
+
// hooks are TRACKED (else savedKitHash is always undefined → every
|
|
139
|
+
// version bump looks "customized" and stale hooks never auto-update)
|
|
124
140
|
|
|
125
141
|
log.blank();
|
|
126
142
|
console.log('--- Installing global hooks ---');
|
|
@@ -133,7 +149,7 @@ export async function initGlobalHooks({ force = false, hooks = null, _globalFile
|
|
|
133
149
|
if (result === 'copied') copied++;
|
|
134
150
|
else if (result === 'identical') identical++;
|
|
135
151
|
else skipped++;
|
|
136
|
-
if (result !== 'skipped') updatedFiles[key] = { kitHash };
|
|
152
|
+
if (result !== 'skipped') { updatedFiles[key] = { kitHash }; entries[key] = { kitHash }; }
|
|
137
153
|
keys.push(key);
|
|
138
154
|
}
|
|
139
155
|
|
|
@@ -153,5 +169,5 @@ export async function initGlobalHooks({ force = false, hooks = null, _globalFile
|
|
|
153
169
|
updatedAt: new Date().toISOString(),
|
|
154
170
|
});
|
|
155
171
|
}
|
|
156
|
-
return keys;
|
|
172
|
+
return { keys, entries };
|
|
157
173
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
setPermissions, fillTemplate,
|
|
15
15
|
verifySettingsJson, COMPONENTS,
|
|
16
16
|
getTemplateDir, installSkillForAgent, installAgentHooks, installAgentRules, resolveSkills, skillAllowed,
|
|
17
|
+
pruneOrphans,
|
|
17
18
|
} from '../lib/installer.js';
|
|
18
19
|
import { resolveHooks } from '../lib/hooks.js';
|
|
19
20
|
import { AGENTS, parseSkillPath, resolveAgents } from '../lib/agents.js';
|
|
@@ -190,6 +191,14 @@ export async function initCommand(path, opts) {
|
|
|
190
191
|
} catch { /* prior agent's file not on disk — don't record a phantom */ }
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
// Migration: drop predecessor files a prior manifest tracked but we no longer
|
|
195
|
+
// install (mf-* / ap-* skills, renamed hooks) so an install over an old version
|
|
196
|
+
// doesn't leave orphaned /mf-* commands beside the new /sp-* ones.
|
|
197
|
+
if (prior?.files) {
|
|
198
|
+
const n = await pruneOrphans(targetDir, prior.files, new Set(Object.keys(manifest.files)));
|
|
199
|
+
if (n) log.info(`Migrated: removed ${n} superseded file(s) from a previous version.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
193
202
|
// --- Permissions ---
|
|
194
203
|
await setPermissions(targetDir);
|
|
195
204
|
|
package/src/lib/claude-global.js
CHANGED
|
@@ -36,6 +36,9 @@ export async function installHookGlobal(srcRel, globalHooksDir, { force = false,
|
|
|
36
36
|
log.same(`~/.claude/hooks/${base} (identical)`);
|
|
37
37
|
return { result: 'identical', kitHash: srcHash };
|
|
38
38
|
}
|
|
39
|
+
// Overwrite only when the on-disk file is one WE wrote (matches the kit hash
|
|
40
|
+
// recorded in the manifest) — i.e. a stale specpipe version, safe to update.
|
|
41
|
+
// Otherwise the user changed it (or we never tracked it) → preserve.
|
|
39
42
|
const savedKitHash = globalFiles[key]?.kitHash;
|
|
40
43
|
if (!(savedKitHash && dstHash === savedKitHash)) {
|
|
41
44
|
log.skip(`~/.claude/hooks/${base} (customized — use --force to overwrite)`);
|
|
@@ -141,6 +144,8 @@ export async function installSkillGlobalForAgent(agentId, skillRelPath, { force
|
|
|
141
144
|
log.same(`${display} (identical)`);
|
|
142
145
|
return { result: 'identical', kitHash: srcHash, key };
|
|
143
146
|
}
|
|
147
|
+
// Overwrite only a stale version we wrote (disk matches the recorded kit hash);
|
|
148
|
+
// otherwise the user customized it (or it's untracked) → preserve.
|
|
144
149
|
const savedKitHash = globalFiles[key]?.kitHash;
|
|
145
150
|
if (!(savedKitHash && dstHash === savedKitHash)) {
|
|
146
151
|
log.skip(`${display} (customized — use --force to overwrite)`);
|
package/src/lib/installer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFile as fsCopyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { copyFile as fsCopyFile, mkdir, readFile, writeFile, unlink, rmdir } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join, dirname, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -150,6 +150,36 @@ export async function installFile(relativePath, targetDir, { force = false } = {
|
|
|
150
150
|
return 'copied';
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Migration prune: delete files a PRIOR manifest tracked that the new install no
|
|
155
|
+
* longer wants — e.g. the predecessor `mf-*` (claude-devkit) / `ap-*` (agentpipe)
|
|
156
|
+
* skills, or renamed/removed hooks. Safe because it only touches paths the kit
|
|
157
|
+
* itself recorded as installed; a user's own files (e.g. a personal `mf-commit`
|
|
158
|
+
* skill that was never in our manifest) are never in `priorFiles`, so untouched.
|
|
159
|
+
* Skips preserved paths and the user's docs/. Cleans up emptied dirs.
|
|
160
|
+
* @returns {Promise<number>} count pruned
|
|
161
|
+
*/
|
|
162
|
+
export async function pruneOrphans(targetDir, priorFiles, keepSet, { preserve = ['.claude/CLAUDE.md'] } = {}) {
|
|
163
|
+
let pruned = 0;
|
|
164
|
+
const dirs = new Set();
|
|
165
|
+
for (const rel of Object.keys(priorFiles || {})) {
|
|
166
|
+
if (keepSet.has(rel) || preserve.includes(rel) || rel.startsWith('docs/')) continue;
|
|
167
|
+
const p = join(targetDir, rel);
|
|
168
|
+
if (!existsSync(p)) continue;
|
|
169
|
+
try {
|
|
170
|
+
await unlink(p);
|
|
171
|
+
log.del(`${rel} (legacy — superseded, removed)`);
|
|
172
|
+
pruned++;
|
|
173
|
+
let d = dirname(rel);
|
|
174
|
+
while (d && d !== '.' && d !== '/') { dirs.add(d); d = dirname(d); }
|
|
175
|
+
} catch { /* ignore */ }
|
|
176
|
+
}
|
|
177
|
+
for (const d of [...dirs].sort((a, b) => b.split('/').length - a.split('/').length)) {
|
|
178
|
+
try { await rmdir(join(targetDir, d)); } catch { /* not empty / missing */ }
|
|
179
|
+
}
|
|
180
|
+
return pruned;
|
|
181
|
+
}
|
|
182
|
+
|
|
153
183
|
// Per-agent install (emit skills + guardrails) lives in agent-install.js;
|
|
154
184
|
// re-exported here so callers keep importing from installer.js.
|
|
155
185
|
export {
|