gsd-lite 0.7.6 → 0.7.8
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/gsd-auto-update.cjs +34 -0
- package/hooks/gsd-session-init.cjs +139 -0
- package/install.js +45 -2
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "gsd",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "AI orchestration tool — GSD management shell + Superpowers quality core. 5 commands, 4 agents, 5 workflows, MCP server, context monitoring.",
|
|
16
|
-
"version": "0.7.
|
|
16
|
+
"version": "0.7.8",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
|
@@ -29,6 +29,35 @@ const LOCK_STALE_MS = 10_000;
|
|
|
29
29
|
const LOCK_RETRY_MS = 50;
|
|
30
30
|
const LOCK_MAX_RETRIES = 100;
|
|
31
31
|
|
|
32
|
+
// ── Orphan Detection ───────────────────────────────────────
|
|
33
|
+
// Mirrors gsd-session-init.cjs Phase 0. When the plugin was removed via
|
|
34
|
+
// /plugin uninstall but install.js-written state (hook files, runtime dir,
|
|
35
|
+
// settings.json) survives, getInstallMode() falls through to 'manual' and a
|
|
36
|
+
// new GitHub release would re-trigger install.js — resurrecting the plugin.
|
|
37
|
+
// Guard here so checkForUpdate is a no-op until the orphan is cleaned up
|
|
38
|
+
// (session-init Phase 0 handles the cleanup itself).
|
|
39
|
+
function isOrphan() {
|
|
40
|
+
const installModeMarker = path.join(claudeDir, 'gsd', '.install-mode');
|
|
41
|
+
let mode = null;
|
|
42
|
+
try { mode = fs.readFileSync(installModeMarker, 'utf8').trim(); } catch { /* missing → fall through */ }
|
|
43
|
+
if (mode === 'manual') return false;
|
|
44
|
+
if (mode === 'plugin') {
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(fs.readFileSync(path.join(claudeDir, 'plugins', 'installed_plugins.json'), 'utf8'));
|
|
47
|
+
return !data.plugins?.['gsd@gsd'];
|
|
48
|
+
} catch { return false; }
|
|
49
|
+
}
|
|
50
|
+
// Pre-marker fallback: orphan iff every cached version has .orphaned_at.
|
|
51
|
+
const cacheBase = path.join(claudeDir, 'plugins', 'cache', 'gsd', 'gsd');
|
|
52
|
+
if (!fs.existsSync(cacheBase)) return false;
|
|
53
|
+
try {
|
|
54
|
+
const dirs = fs.readdirSync(cacheBase, { withFileTypes: true })
|
|
55
|
+
.filter(e => e.isDirectory() && /^\d+\.\d+\.\d+/.test(e.name));
|
|
56
|
+
if (dirs.length === 0) return false;
|
|
57
|
+
return dirs.every(d => fs.existsSync(path.join(cacheBase, d.name, '.orphaned_at')));
|
|
58
|
+
} catch { return false; }
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
// ── Main Entry ─────────────────────────────────────────────
|
|
33
62
|
async function checkForUpdate(options = {}) {
|
|
34
63
|
const {
|
|
@@ -43,6 +72,10 @@ async function checkForUpdate(options = {}) {
|
|
|
43
72
|
const installMode = getInstallMode();
|
|
44
73
|
|
|
45
74
|
try {
|
|
75
|
+
if (isOrphan()) {
|
|
76
|
+
if (verbose) console.log('Skipping update check (plugin uninstalled — orphan state)');
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
46
79
|
if (!force && shouldSkipUpdateCheck()) {
|
|
47
80
|
if (verbose) console.log('Skipping update check (dev mode or auto-update in progress)');
|
|
48
81
|
return null;
|
|
@@ -643,6 +676,7 @@ module.exports = {
|
|
|
643
676
|
compareVersions,
|
|
644
677
|
getInstallMode,
|
|
645
678
|
isDevMode,
|
|
679
|
+
isOrphan,
|
|
646
680
|
shouldCheck,
|
|
647
681
|
shouldSkipUpdateCheck,
|
|
648
682
|
validateExtractedPackage,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GSD-Lite SessionStart hook
|
|
3
|
+
// 0. Orphan self-cleanup: if /plugin uninstall removed the plugin but left
|
|
4
|
+
// install.js-written state behind, run inline cleanup and exit.
|
|
3
5
|
// 1. Cleans up stale temp files (throttled to once/day).
|
|
4
6
|
// 2. Auto-registers statusLine in settings.json if not already configured.
|
|
5
7
|
// 3. Self-heals .mcp.json if missing.
|
|
@@ -16,6 +18,143 @@ const os = require('node:os');
|
|
|
16
18
|
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
17
19
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
18
20
|
|
|
21
|
+
// ── Phase 0: Orphan self-cleanup ──
|
|
22
|
+
// /plugin uninstall only touches installed_plugins.json + enabledPlugins; it
|
|
23
|
+
// leaves hook scripts, settings.json registrations, the runtime dir, and the
|
|
24
|
+
// composite statusline registry in place — so hooks keep firing and Phases
|
|
25
|
+
// 2/5 below will self-heal the stale state on every session. Detect that case
|
|
26
|
+
// here and remove our footprint before any other phase runs.
|
|
27
|
+
function isOrphan() {
|
|
28
|
+
// .install-mode marker (written by install.js ≥ 0.7.7) is authoritative.
|
|
29
|
+
const installModeMarker = path.join(claudeDir, 'gsd', '.install-mode');
|
|
30
|
+
let mode = null;
|
|
31
|
+
try { mode = fs.readFileSync(installModeMarker, 'utf8').trim(); } catch { /* missing → fall through */ }
|
|
32
|
+
if (mode === 'manual') return false; // npx install never enters /plugin uninstall path
|
|
33
|
+
if (mode === 'plugin') {
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(fs.readFileSync(path.join(claudeDir, 'plugins', 'installed_plugins.json'), 'utf8'));
|
|
36
|
+
return !data.plugins?.['gsd@gsd'];
|
|
37
|
+
} catch { return false; } // registry unreadable → don't assume orphan
|
|
38
|
+
}
|
|
39
|
+
// Pre-marker installs: fallback heuristic. Claude Code stamps
|
|
40
|
+
// .orphaned_at inside cache version dirs when the entry is removed from
|
|
41
|
+
// installed_plugins.json. Orphan iff every cached version has the marker.
|
|
42
|
+
const cacheBase = path.join(claudeDir, 'plugins', 'cache', 'gsd', 'gsd');
|
|
43
|
+
if (!fs.existsSync(cacheBase)) return false;
|
|
44
|
+
try {
|
|
45
|
+
const dirs = fs.readdirSync(cacheBase, { withFileTypes: true })
|
|
46
|
+
.filter(e => e.isDirectory() && /^\d+\.\d+\.\d+/.test(e.name));
|
|
47
|
+
if (dirs.length === 0) return false;
|
|
48
|
+
return dirs.every(d => fs.existsSync(path.join(cacheBase, d.name, '.orphaned_at')));
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function atomicWriteJson(filePath, value) {
|
|
53
|
+
const tmp = filePath + `.gsd-orphan-${process.pid}-${Date.now()}.tmp`;
|
|
54
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n');
|
|
55
|
+
fs.renameSync(tmp, filePath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cleanupOrphan() {
|
|
59
|
+
// 1. Composite statusLine registry — call removeProvider BEFORE deleting
|
|
60
|
+
// the lib file it lives in.
|
|
61
|
+
try {
|
|
62
|
+
const compositeLib = path.join(claudeDir, 'hooks', 'lib', 'statusline-composite.cjs');
|
|
63
|
+
if (fs.existsSync(compositeLib)) {
|
|
64
|
+
const { removeProvider } = require(compositeLib);
|
|
65
|
+
removeProvider();
|
|
66
|
+
}
|
|
67
|
+
} catch { /* best effort */ }
|
|
68
|
+
|
|
69
|
+
// 2. settings.json — mirror uninstall.js logic.
|
|
70
|
+
try {
|
|
71
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
72
|
+
let changed = false;
|
|
73
|
+
for (const name of ['gsd', 'gsd-lite']) {
|
|
74
|
+
if (settings.mcpServers?.[name]) { delete settings.mcpServers[name]; changed = true; }
|
|
75
|
+
const pluginKey = `${name}@${name}`;
|
|
76
|
+
if (settings.enabledPlugins?.[pluginKey]) { delete settings.enabledPlugins[pluginKey]; changed = true; }
|
|
77
|
+
if (settings.extraKnownMarketplaces?.[name]) { delete settings.extraKnownMarketplaces[name]; changed = true; }
|
|
78
|
+
}
|
|
79
|
+
if (settings.extraKnownMarketplaces && Object.keys(settings.extraKnownMarketplaces).length === 0) {
|
|
80
|
+
delete settings.extraKnownMarketplaces;
|
|
81
|
+
}
|
|
82
|
+
if (settings.statusLine?.command?.includes('gsd-statusline')
|
|
83
|
+
|| settings.statusLine?.command?.includes('context-monitor.js')) {
|
|
84
|
+
delete settings.statusLine;
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
if (settings.hooks) {
|
|
88
|
+
if (typeof settings.hooks.StatusLine === 'string'
|
|
89
|
+
&& (settings.hooks.StatusLine.includes('gsd-statusline')
|
|
90
|
+
|| settings.hooks.StatusLine.includes('context-monitor.js'))) {
|
|
91
|
+
delete settings.hooks.StatusLine;
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
for (const [hookType, identifier] of [
|
|
95
|
+
['PostToolUse', 'gsd-context-monitor'],
|
|
96
|
+
['PostToolUse', 'context-monitor.js'],
|
|
97
|
+
['SessionStart', 'gsd-session-init'],
|
|
98
|
+
['Stop', 'gsd-session-stop'],
|
|
99
|
+
]) {
|
|
100
|
+
if (Array.isArray(settings.hooks[hookType])) {
|
|
101
|
+
const before = settings.hooks[hookType].length;
|
|
102
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter(e =>
|
|
103
|
+
!e.hooks?.some(h => h.command?.includes(identifier)));
|
|
104
|
+
if (settings.hooks[hookType].length < before) changed = true;
|
|
105
|
+
if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
|
|
106
|
+
} else if (typeof settings.hooks[hookType] === 'string'
|
|
107
|
+
&& settings.hooks[hookType].includes(identifier)) {
|
|
108
|
+
delete settings.hooks[hookType];
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (changed) atomicWriteJson(settingsPath, settings);
|
|
114
|
+
} catch { /* best effort */ }
|
|
115
|
+
|
|
116
|
+
// 3. plugins/known_marketplaces.json
|
|
117
|
+
try {
|
|
118
|
+
const known = path.join(claudeDir, 'plugins', 'known_marketplaces.json');
|
|
119
|
+
const data = JSON.parse(fs.readFileSync(known, 'utf8'));
|
|
120
|
+
let dirty = false;
|
|
121
|
+
for (const n of ['gsd', 'gsd-lite']) {
|
|
122
|
+
if (n in data) { delete data[n]; dirty = true; }
|
|
123
|
+
}
|
|
124
|
+
if (dirty) atomicWriteJson(known, data);
|
|
125
|
+
} catch { /* best effort */ }
|
|
126
|
+
|
|
127
|
+
// 4. Hook script files
|
|
128
|
+
for (const name of ['context-monitor.js', 'gsd-statusline.cjs', 'gsd-context-monitor.cjs', 'gsd-auto-update.cjs', 'gsd-session-stop.cjs']) {
|
|
129
|
+
try { fs.rmSync(path.join(claudeDir, 'hooks', name), { force: true }); } catch { /* best effort */ }
|
|
130
|
+
}
|
|
131
|
+
// 5. Hook lib files (GSD-owned only — don't touch other plugins' libs)
|
|
132
|
+
for (const lib of ['gsd-finder.cjs', 'statusline-composite.cjs', 'semver-sort.cjs']) {
|
|
133
|
+
try { fs.rmSync(path.join(claudeDir, 'hooks', 'lib', lib), { force: true }); } catch { /* best effort */ }
|
|
134
|
+
}
|
|
135
|
+
// 6. Runtime dir + plugin marketplace + cache dirs (current + legacy names)
|
|
136
|
+
for (const dir of [
|
|
137
|
+
path.join(claudeDir, 'gsd'),
|
|
138
|
+
path.join(claudeDir, 'gsd-lite'),
|
|
139
|
+
path.join(claudeDir, 'plugins', 'marketplaces', 'gsd'),
|
|
140
|
+
path.join(claudeDir, 'plugins', 'marketplaces', 'gsd-lite'),
|
|
141
|
+
path.join(claudeDir, 'plugins', 'cache', 'gsd'),
|
|
142
|
+
path.join(claudeDir, 'plugins', 'cache', 'gsd-lite'),
|
|
143
|
+
]) {
|
|
144
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
145
|
+
}
|
|
146
|
+
// 7. Self-removal — delete this script last. The running process keeps the
|
|
147
|
+
// file handle until exit (POSIX); on Windows this may fail silently and
|
|
148
|
+
// Phase 0's next-session check will retry.
|
|
149
|
+
try { fs.rmSync(path.join(claudeDir, 'hooks', 'gsd-session-init.cjs'), { force: true }); } catch { /* best effort */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isOrphan()) {
|
|
153
|
+
console.log('⚠ GSD-Lite plugin uninstalled — cleaning up orphaned hooks and runtime.');
|
|
154
|
+
try { cleanupOrphan(); } catch { /* best effort — never block session start */ }
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
19
158
|
// Safety: exit after 4s regardless (hook timeout is 5s)
|
|
20
159
|
setTimeout(() => process.exit(0), 4000).unref();
|
|
21
160
|
|
package/install.js
CHANGED
|
@@ -210,7 +210,24 @@ export function main() {
|
|
|
210
210
|
|
|
211
211
|
// 6. Stable runtime for MCP server
|
|
212
212
|
copyDir(join(__dirname, 'src'), join(RUNTIME_DIR, 'src'), 'runtime/src → ~/.claude/gsd/src/');
|
|
213
|
-
|
|
213
|
+
// Write a sanitized package.json: strip dev-only npm lifecycle scripts
|
|
214
|
+
// (prepare/prepublishOnly/version use POSIX shell + dev tooling absent from
|
|
215
|
+
// the runtime). Leaving them in means a later manual `npm install` in
|
|
216
|
+
// ~/.claude/gsd fails under cmd.exe on Windows (issue #2).
|
|
217
|
+
if (DRY_RUN) {
|
|
218
|
+
log(' [dry-run] Would write runtime/package.json (dev scripts stripped)');
|
|
219
|
+
} else {
|
|
220
|
+
const runtimePkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
221
|
+
delete runtimePkg.scripts;
|
|
222
|
+
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
223
|
+
writeFileSync(join(RUNTIME_DIR, 'package.json'), JSON.stringify(runtimePkg, null, 2) + '\n');
|
|
224
|
+
log(' ✓ runtime/package.json → ~/.claude/gsd/package.json (scripts stripped)');
|
|
225
|
+
}
|
|
226
|
+
// Copy uninstall.js so the SessionStart hook's Phase 0 orphan-cleanup can
|
|
227
|
+
// spawn it when /plugin uninstall has removed the plugin without running our
|
|
228
|
+
// uninstaller. Without this, hooks/runtime/settings.json entries written by
|
|
229
|
+
// install.js outlive the plugin and keep firing.
|
|
230
|
+
copyFile(join(__dirname, 'uninstall.js'), join(RUNTIME_DIR, 'uninstall.js'), 'runtime/uninstall.js → ~/.claude/gsd/uninstall.js');
|
|
214
231
|
// Copy lock file so `npm ci` works when node_modules are not present (npx scenario)
|
|
215
232
|
const lockFile = join(__dirname, 'package-lock.json');
|
|
216
233
|
if (existsSync(lockFile)) {
|
|
@@ -225,7 +242,12 @@ export function main() {
|
|
|
225
242
|
log(' ⧗ Installing runtime dependencies...');
|
|
226
243
|
const lockFile = join(RUNTIME_DIR, 'package-lock.json');
|
|
227
244
|
const hasLockFile = existsSync(lockFile);
|
|
228
|
-
|
|
245
|
+
// --ignore-scripts: the runtime install only needs node_modules. Skipping
|
|
246
|
+
// lifecycle scripts avoids running the dev-only POSIX `prepare` git-hook
|
|
247
|
+
// setup, which fails under cmd.exe on Windows (issue #2).
|
|
248
|
+
const installCmd = hasLockFile
|
|
249
|
+
? 'npm ci --omit=dev --ignore-scripts'
|
|
250
|
+
: 'npm install --omit=dev --no-fund --no-audit --ignore-scripts';
|
|
229
251
|
try {
|
|
230
252
|
execSync(installCmd, { cwd: RUNTIME_DIR, stdio: 'pipe' });
|
|
231
253
|
log(' ✓ runtime dependencies installed');
|
|
@@ -289,6 +311,27 @@ export function main() {
|
|
|
289
311
|
if (statusLineRegistered || hooksRegistered) {
|
|
290
312
|
log(' ✓ GSD-Lite hooks registered in settings.json');
|
|
291
313
|
}
|
|
314
|
+
|
|
315
|
+
// Record install mode so SessionStart's Phase 0 orphan-cleanup can
|
|
316
|
+
// distinguish "plugin uninstalled" from "npx install". Without this marker
|
|
317
|
+
// the hook falls back to the .orphaned_at cache heuristic, which is fine
|
|
318
|
+
// for pre-marker users but more guess-y.
|
|
319
|
+
try {
|
|
320
|
+
const installModeMarker = join(RUNTIME_DIR, '.install-mode');
|
|
321
|
+
writeFileSync(installModeMarker, (isPluginInstall ? 'plugin' : 'manual') + '\n');
|
|
322
|
+
} catch { /* best effort — marker missing falls back to heuristic */ }
|
|
323
|
+
|
|
324
|
+
// Clear stale .orphaned_at from the current plugin cache version, in case
|
|
325
|
+
// the user previously uninstalled (Claude Code stamps it) and is now
|
|
326
|
+
// reinstalling via /plugin. Leaving it would make orphan-cleanup mis-fire
|
|
327
|
+
// on the next session.
|
|
328
|
+
if (isPluginInstall) {
|
|
329
|
+
try {
|
|
330
|
+
const currentVersion = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8')).version;
|
|
331
|
+
const orphanMarker = join(CLAUDE_DIR, 'plugins', 'cache', 'gsd', 'gsd', currentVersion, '.orphaned_at');
|
|
332
|
+
if (existsSync(orphanMarker)) rmSync(orphanMarker, { force: true });
|
|
333
|
+
} catch { /* best effort */ }
|
|
334
|
+
}
|
|
292
335
|
} else {
|
|
293
336
|
log(' [dry-run] Would register MCP server in settings.json');
|
|
294
337
|
}
|