gsd-lite 0.5.0 → 0.5.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/commands/start.md +9 -0
- package/hooks/gsd-session-init.cjs +33 -2
- package/hooks/gsd-statusline.cjs +4 -1
- package/install.js +28 -6
- package/package.json +1 -1
- package/src/schema.js +24 -5
|
@@ -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.5.
|
|
16
|
+
"version": "0.5.2",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
package/commands/start.md
CHANGED
|
@@ -10,6 +10,15 @@ argument-hint: Optional feature or project description
|
|
|
10
10
|
|
|
11
11
|
<process>
|
|
12
12
|
|
|
13
|
+
## STEP 0 — 已有项目检测
|
|
14
|
+
|
|
15
|
+
调用 `gsd health` 工具。如果返回 state_exists=true:
|
|
16
|
+
- 告知用户: "检测到进行中的 GSD 项目。"
|
|
17
|
+
- 提供选项:
|
|
18
|
+
- (a) 恢复执行 → 转到 `/gsd:resume`
|
|
19
|
+
- (b) 重新开始 → 继续 STEP 1(现有 state.json 将被覆盖)
|
|
20
|
+
- 等待用户选择后再继续
|
|
21
|
+
|
|
13
22
|
## STEP 1 — 语言检测
|
|
14
23
|
|
|
15
24
|
用户输入语言 = 后续所有输出语言。不需要读 CLAUDE.md 来判断语言。
|
|
@@ -66,7 +66,38 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
66
66
|
}
|
|
67
67
|
} catch { /* silent */ }
|
|
68
68
|
|
|
69
|
-
// ── Phase 3:
|
|
69
|
+
// ── Phase 3: Self-heal .mcp.json in plugin directories ──
|
|
70
|
+
// If .mcp.json is missing (e.g. git operations deleted it), regenerate it
|
|
71
|
+
// so the plugin system can register the GSD MCP server.
|
|
72
|
+
try {
|
|
73
|
+
const pluginsPath = path.join(claudeDir, 'plugins', 'installed_plugins.json');
|
|
74
|
+
if (fs.existsSync(pluginsPath)) {
|
|
75
|
+
const plugins = JSON.parse(fs.readFileSync(pluginsPath, 'utf8'));
|
|
76
|
+
const gsdEntry = plugins.plugins?.['gsd@gsd']?.[0];
|
|
77
|
+
if (gsdEntry) {
|
|
78
|
+
const mcpContent = JSON.stringify({
|
|
79
|
+
mcpServers: {
|
|
80
|
+
gsd: { command: 'node', args: ['${CLAUDE_PLUGIN_ROOT}/launcher.js'] },
|
|
81
|
+
},
|
|
82
|
+
}, null, 2) + '\n';
|
|
83
|
+
// Check marketplace dir
|
|
84
|
+
const marketplaceDir = path.join(claudeDir, 'plugins', 'marketplaces', 'gsd');
|
|
85
|
+
const marketplaceMcp = path.join(marketplaceDir, '.mcp.json');
|
|
86
|
+
if (fs.existsSync(marketplaceDir) && !fs.existsSync(marketplaceMcp)) {
|
|
87
|
+
fs.writeFileSync(marketplaceMcp, mcpContent);
|
|
88
|
+
}
|
|
89
|
+
// Check plugin cache dir
|
|
90
|
+
if (gsdEntry.installPath) {
|
|
91
|
+
const cacheMcp = path.join(gsdEntry.installPath, '.mcp.json');
|
|
92
|
+
if (fs.existsSync(gsdEntry.installPath) && !fs.existsSync(cacheMcp)) {
|
|
93
|
+
fs.writeFileSync(cacheMcp, mcpContent);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch { /* silent */ }
|
|
99
|
+
|
|
100
|
+
// ── Phase 4: Show notification from previous background auto-update ──
|
|
70
101
|
try {
|
|
71
102
|
const notifPath = path.join(claudeDir, 'gsd', 'runtime', 'update-notification.json');
|
|
72
103
|
if (fs.existsSync(notifPath)) {
|
|
@@ -82,7 +113,7 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
82
113
|
}
|
|
83
114
|
} catch { /* silent */ }
|
|
84
115
|
|
|
85
|
-
// ── Phase
|
|
116
|
+
// ── Phase 5: Spawn background auto-update (non-blocking) ──
|
|
86
117
|
// Detached child handles check + download + install; throttled by shouldCheck()
|
|
87
118
|
try {
|
|
88
119
|
const { spawn } = require('node:child_process');
|
package/hooks/gsd-statusline.cjs
CHANGED
|
@@ -49,7 +49,10 @@ process.stdin.on('end', () => {
|
|
|
49
49
|
if (state.current_task && state.current_phase) {
|
|
50
50
|
const phase = (state.phases || []).find(p => p.id === state.current_phase);
|
|
51
51
|
const t = phase?.todo?.find(t => t.id === state.current_task);
|
|
52
|
-
if (t)
|
|
52
|
+
if (t) {
|
|
53
|
+
const name = t.name.length > 40 ? t.name.substring(0, 40) + '...' : t.name;
|
|
54
|
+
task = `${t.id} ${name}`;
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
} catch {
|
|
55
58
|
// No state.json or parse error — skip task display
|
package/install.js
CHANGED
|
@@ -23,6 +23,16 @@ const HOOK_REGISTRY = [
|
|
|
23
23
|
|
|
24
24
|
function log(msg) { console.log(msg); }
|
|
25
25
|
|
|
26
|
+
function isInstalledAsPlugin(claudeDir) {
|
|
27
|
+
try {
|
|
28
|
+
const pluginsPath = join(claudeDir, 'plugins', 'installed_plugins.json');
|
|
29
|
+
const data = JSON.parse(readFileSync(pluginsPath, 'utf-8'));
|
|
30
|
+
return !!data.plugins?.['gsd@gsd'];
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function registerStatusLine(settings, statuslineScriptPath) {
|
|
27
37
|
const command = `node ${JSON.stringify(statuslineScriptPath)}`;
|
|
28
38
|
// Don't overwrite non-GSD statusLine
|
|
@@ -142,8 +152,11 @@ export function main() {
|
|
|
142
152
|
log(' [dry-run] Would install runtime dependencies');
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
// 8. Register MCP server in settings.json
|
|
155
|
+
// 8. Register MCP server + hooks in settings.json
|
|
156
|
+
// When installed as a plugin, the plugin system handles MCP via .mcp.json,
|
|
157
|
+
// so we skip manual MCP registration to avoid name collisions.
|
|
146
158
|
const settingsPath = join(CLAUDE_DIR, 'settings.json');
|
|
159
|
+
const isPluginInstall = isInstalledAsPlugin(CLAUDE_DIR);
|
|
147
160
|
if (!DRY_RUN) {
|
|
148
161
|
let settings = {};
|
|
149
162
|
try {
|
|
@@ -157,10 +170,20 @@ export function main() {
|
|
|
157
170
|
if (!settings.mcpServers) settings.mcpServers = {};
|
|
158
171
|
// Remove legacy "gsd-lite" server entry from older versions
|
|
159
172
|
delete settings.mcpServers['gsd-lite'];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
|
|
174
|
+
if (isPluginInstall) {
|
|
175
|
+
// Plugin system handles MCP via .mcp.json — remove stale manual entry
|
|
176
|
+
if (settings.mcpServers.gsd) {
|
|
177
|
+
delete settings.mcpServers.gsd;
|
|
178
|
+
log(' ✓ Removed manual MCP entry (plugin .mcp.json handles registration)');
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
settings.mcpServers.gsd = {
|
|
182
|
+
command: 'node',
|
|
183
|
+
args: [join(RUNTIME_DIR, 'src', 'server.js')],
|
|
184
|
+
};
|
|
185
|
+
log(' ✓ MCP server registered in settings.json');
|
|
186
|
+
}
|
|
164
187
|
|
|
165
188
|
// Register statusLine (top-level setting) and hooks
|
|
166
189
|
if (!settings.hooks) settings.hooks = {};
|
|
@@ -174,7 +197,6 @@ export function main() {
|
|
|
174
197
|
const tmpSettings = settingsPath + `.${process.pid}-${Date.now()}.tmp`;
|
|
175
198
|
writeFileSync(tmpSettings, JSON.stringify(settings, null, 2) + '\n');
|
|
176
199
|
renameSync(tmpSettings, settingsPath);
|
|
177
|
-
log(' ✓ MCP server registered in settings.json');
|
|
178
200
|
if (statusLineRegistered || hooksRegistered) {
|
|
179
201
|
log(' ✓ GSD-Lite hooks registered in settings.json');
|
|
180
202
|
}
|
package/package.json
CHANGED
package/src/schema.js
CHANGED
|
@@ -98,8 +98,12 @@ export function validateResearchDecisionIndex(decisionIndex, requiredIds = []) {
|
|
|
98
98
|
if ('source' in entry && (typeof entry.source !== 'string' || entry.source.length === 0)) {
|
|
99
99
|
errors.push(`decision_index.${id}.source must be a non-empty string`);
|
|
100
100
|
}
|
|
101
|
-
if ('expires_at' in entry
|
|
102
|
-
|
|
101
|
+
if ('expires_at' in entry) {
|
|
102
|
+
if (typeof entry.expires_at !== 'string' || entry.expires_at.length === 0) {
|
|
103
|
+
errors.push(`decision_index.${id}.expires_at must be a non-empty string`);
|
|
104
|
+
} else if (Number.isNaN(Date.parse(entry.expires_at))) {
|
|
105
|
+
errors.push(`decision_index.${id}.expires_at must be a valid ISO 8601 date (got "${entry.expires_at}")`);
|
|
106
|
+
}
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -169,6 +173,12 @@ export function validateStateUpdate(state, updates) {
|
|
|
169
173
|
if (updates.current_review !== null && !isPlainObject(updates.current_review)) {
|
|
170
174
|
errors.push('current_review must be an object or null');
|
|
171
175
|
}
|
|
176
|
+
if (isPlainObject(updates.current_review) && 'scope' in updates.current_review) {
|
|
177
|
+
const validScopes = ['task', 'phase'];
|
|
178
|
+
if (!validScopes.includes(updates.current_review.scope)) {
|
|
179
|
+
errors.push(`current_review.scope must be one of: ${validScopes.join(', ')} (got "${updates.current_review.scope}")`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
172
182
|
break;
|
|
173
183
|
case 'git_head':
|
|
174
184
|
if (updates.git_head !== null && typeof updates.git_head !== 'string') {
|
|
@@ -306,9 +316,12 @@ export function validateState(state) {
|
|
|
306
316
|
if ('volatility' in state.research && !['low', 'medium', 'high'].includes(state.research.volatility)) {
|
|
307
317
|
errors.push('research.volatility must be low|medium|high');
|
|
308
318
|
}
|
|
309
|
-
if ('expires_at' in state.research
|
|
310
|
-
|
|
311
|
-
|
|
319
|
+
if ('expires_at' in state.research) {
|
|
320
|
+
if (typeof state.research.expires_at !== 'string' || state.research.expires_at.length === 0) {
|
|
321
|
+
errors.push('research.expires_at must be a non-empty string');
|
|
322
|
+
} else if (Number.isNaN(Date.parse(state.research.expires_at))) {
|
|
323
|
+
errors.push(`research.expires_at must be a valid ISO 8601 date (got "${state.research.expires_at}")`);
|
|
324
|
+
}
|
|
312
325
|
}
|
|
313
326
|
if ('files' in state.research && !Array.isArray(state.research.files)) {
|
|
314
327
|
errors.push('research.files must be an array');
|
|
@@ -335,6 +348,12 @@ export function validateState(state) {
|
|
|
335
348
|
if (state.current_review !== null && !isPlainObject(state.current_review)) {
|
|
336
349
|
errors.push('current_review must be an object or null');
|
|
337
350
|
}
|
|
351
|
+
if (isPlainObject(state.current_review) && 'scope' in state.current_review) {
|
|
352
|
+
const validScopes = ['task', 'phase'];
|
|
353
|
+
if (!validScopes.includes(state.current_review.scope)) {
|
|
354
|
+
errors.push(`current_review.scope must be one of: ${validScopes.join(', ')} (got "${state.current_review.scope}")`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
338
357
|
if (!isPlainObject(state.evidence)) {
|
|
339
358
|
errors.push('evidence must be an object');
|
|
340
359
|
} else {
|