gsd-lite 0.2.0 → 0.3.1
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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.mcp.json +5 -3
- package/README.md +7 -6
- package/agents/{gsd-debugger.md → debugger.md} +2 -2
- package/agents/{gsd-executor.md → executor.md} +2 -2
- package/agents/{gsd-researcher.md → researcher.md} +2 -2
- package/agents/{gsd-reviewer.md → reviewer.md} +2 -2
- package/cli.js +5 -5
- package/commands/prd.md +291 -0
- package/commands/{gsd-resume.md → resume.md} +7 -8
- package/commands/{gsd-start.md → start.md} +9 -10
- package/commands/{gsd-status.md → status.md} +0 -1
- package/commands/{gsd-stop.md → stop.md} +0 -1
- package/hooks/context-monitor.js +8 -28
- package/hooks/gsd-context-monitor.cjs +124 -0
- package/hooks/gsd-session-init.cjs +61 -0
- package/hooks/gsd-statusline.cjs +114 -0
- package/hooks/hooks.json +15 -2
- package/install.js +49 -24
- package/launcher.js +25 -0
- package/package.json +4 -3
- package/references/questioning.md +1 -1
- package/src/schema.js +11 -5
- package/src/server.js +45 -25
- package/src/tools/orchestrator.js +19 -5
- package/src/tools/state.js +10 -7
- package/src/tools/verify.js +6 -5
- package/src/utils.js +30 -13
- package/uninstall.js +84 -22
- package/workflows/debugging.md +1 -1
- package/workflows/deviation-rules.md +1 -1
- package/workflows/research.md +1 -1
- package/workflows/review-cycle.md +1 -1
- package/workflows/tdd-cycle.md +1 -1
- package/commands/gsd-prd.md +0 -154
package/src/server.js
CHANGED
|
@@ -2,7 +2,11 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { init, read, update, phaseComplete } from './tools/state.js';
|
|
7
|
+
|
|
8
|
+
const _require = createRequire(import.meta.url);
|
|
9
|
+
const PKG_VERSION = _require('../package.json').version;
|
|
6
10
|
import {
|
|
7
11
|
handleDebuggerResult,
|
|
8
12
|
handleExecutorResult,
|
|
@@ -12,13 +16,13 @@ import {
|
|
|
12
16
|
} from './tools/orchestrator.js';
|
|
13
17
|
|
|
14
18
|
const server = new Server(
|
|
15
|
-
{ name: 'gsd
|
|
19
|
+
{ name: 'gsd', version: PKG_VERSION },
|
|
16
20
|
{ capabilities: { tools: {} } }
|
|
17
21
|
);
|
|
18
22
|
|
|
19
23
|
const TOOLS = [
|
|
20
24
|
{
|
|
21
|
-
name: '
|
|
25
|
+
name: 'health',
|
|
22
26
|
description: 'Health check: returns server status and whether .gsd state exists',
|
|
23
27
|
inputSchema: {
|
|
24
28
|
type: 'object',
|
|
@@ -26,7 +30,7 @@ const TOOLS = [
|
|
|
26
30
|
},
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
|
-
name: '
|
|
33
|
+
name: 'state-init',
|
|
30
34
|
description: 'Initialize .gsd/ directory with state.json, plan.md, and phases/*.md',
|
|
31
35
|
inputSchema: {
|
|
32
36
|
type: 'object',
|
|
@@ -38,9 +42,25 @@ const TOOLS = [
|
|
|
38
42
|
items: {
|
|
39
43
|
type: 'object',
|
|
40
44
|
properties: {
|
|
41
|
-
name: { type: 'string' },
|
|
42
|
-
tasks: {
|
|
45
|
+
name: { type: 'string', description: 'Phase name' },
|
|
46
|
+
tasks: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description: 'Task definitions',
|
|
49
|
+
items: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
name: { type: 'string', description: 'Task name (required)' },
|
|
53
|
+
index: { type: 'number', description: 'Task index within phase (default: auto)' },
|
|
54
|
+
level: { type: 'string', description: 'Complexity level: L0/L1/L2/L3 (default: L1)' },
|
|
55
|
+
requires: { type: 'array', description: 'Dependency list (default: [])' },
|
|
56
|
+
review_required: { type: 'boolean', description: 'Whether review is needed (default: true)' },
|
|
57
|
+
verification_required: { type: 'boolean', description: 'Whether verification is needed (default: true)' },
|
|
58
|
+
},
|
|
59
|
+
required: ['name'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
43
62
|
},
|
|
63
|
+
required: ['name'],
|
|
44
64
|
},
|
|
45
65
|
},
|
|
46
66
|
research: { type: 'boolean', description: 'Whether research directory is needed' },
|
|
@@ -49,7 +69,7 @@ const TOOLS = [
|
|
|
49
69
|
},
|
|
50
70
|
},
|
|
51
71
|
{
|
|
52
|
-
name: '
|
|
72
|
+
name: 'state-read',
|
|
53
73
|
description: 'Read state.json, optionally filtering to specific fields',
|
|
54
74
|
inputSchema: {
|
|
55
75
|
type: 'object',
|
|
@@ -63,7 +83,7 @@ const TOOLS = [
|
|
|
63
83
|
},
|
|
64
84
|
},
|
|
65
85
|
{
|
|
66
|
-
name: '
|
|
86
|
+
name: 'state-update',
|
|
67
87
|
description: 'Update state.json canonical fields with lifecycle validation',
|
|
68
88
|
inputSchema: {
|
|
69
89
|
type: 'object',
|
|
@@ -77,7 +97,7 @@ const TOOLS = [
|
|
|
77
97
|
},
|
|
78
98
|
},
|
|
79
99
|
{
|
|
80
|
-
name: '
|
|
100
|
+
name: 'phase-complete',
|
|
81
101
|
description: 'Mark a phase as complete after verifying handoff gate conditions',
|
|
82
102
|
inputSchema: {
|
|
83
103
|
type: 'object',
|
|
@@ -100,7 +120,7 @@ const TOOLS = [
|
|
|
100
120
|
},
|
|
101
121
|
},
|
|
102
122
|
{
|
|
103
|
-
name: '
|
|
123
|
+
name: 'orchestrator-resume',
|
|
104
124
|
description: 'Resume the minimal orchestration loop from workflow_mode/current_phase state',
|
|
105
125
|
inputSchema: {
|
|
106
126
|
type: 'object',
|
|
@@ -108,7 +128,7 @@ const TOOLS = [
|
|
|
108
128
|
},
|
|
109
129
|
},
|
|
110
130
|
{
|
|
111
|
-
name: '
|
|
131
|
+
name: 'orchestrator-handle-executor-result',
|
|
112
132
|
description: 'Persist an executor result and determine the next orchestration action',
|
|
113
133
|
inputSchema: {
|
|
114
134
|
type: 'object',
|
|
@@ -119,7 +139,7 @@ const TOOLS = [
|
|
|
119
139
|
},
|
|
120
140
|
},
|
|
121
141
|
{
|
|
122
|
-
name: '
|
|
142
|
+
name: 'orchestrator-handle-debugger-result',
|
|
123
143
|
description: 'Persist a debugger result and determine the next orchestration action',
|
|
124
144
|
inputSchema: {
|
|
125
145
|
type: 'object',
|
|
@@ -130,7 +150,7 @@ const TOOLS = [
|
|
|
130
150
|
},
|
|
131
151
|
},
|
|
132
152
|
{
|
|
133
|
-
name: '
|
|
153
|
+
name: 'orchestrator-handle-researcher-result',
|
|
134
154
|
description: 'Persist a researcher result, write .gsd/research artifacts, and continue orchestration',
|
|
135
155
|
inputSchema: {
|
|
136
156
|
type: 'object',
|
|
@@ -143,7 +163,7 @@ const TOOLS = [
|
|
|
143
163
|
},
|
|
144
164
|
},
|
|
145
165
|
{
|
|
146
|
-
name: '
|
|
166
|
+
name: 'orchestrator-handle-reviewer-result',
|
|
147
167
|
description: 'Persist a reviewer result, update task lifecycles, and determine next orchestration action',
|
|
148
168
|
inputSchema: {
|
|
149
169
|
type: 'object',
|
|
@@ -162,12 +182,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
162
182
|
async function dispatchToolCall(name, args) {
|
|
163
183
|
let result;
|
|
164
184
|
switch (name) {
|
|
165
|
-
case '
|
|
185
|
+
case 'health': {
|
|
166
186
|
const stateResult = await read(args || {});
|
|
167
187
|
result = {
|
|
168
188
|
status: 'ok',
|
|
169
|
-
server: 'gsd
|
|
170
|
-
version:
|
|
189
|
+
server: 'gsd',
|
|
190
|
+
version: PKG_VERSION,
|
|
171
191
|
state_exists: !stateResult.error,
|
|
172
192
|
...(stateResult.error ? {} : {
|
|
173
193
|
project: stateResult.project,
|
|
@@ -178,31 +198,31 @@ async function dispatchToolCall(name, args) {
|
|
|
178
198
|
};
|
|
179
199
|
break;
|
|
180
200
|
}
|
|
181
|
-
case '
|
|
201
|
+
case 'state-init':
|
|
182
202
|
result = await init(args);
|
|
183
203
|
break;
|
|
184
|
-
case '
|
|
204
|
+
case 'state-read':
|
|
185
205
|
result = await read(args || {});
|
|
186
206
|
break;
|
|
187
|
-
case '
|
|
207
|
+
case 'state-update':
|
|
188
208
|
result = await update(args);
|
|
189
209
|
break;
|
|
190
|
-
case '
|
|
210
|
+
case 'phase-complete':
|
|
191
211
|
result = await phaseComplete(args);
|
|
192
212
|
break;
|
|
193
|
-
case '
|
|
213
|
+
case 'orchestrator-resume':
|
|
194
214
|
result = await resumeWorkflow(args || {});
|
|
195
215
|
break;
|
|
196
|
-
case '
|
|
216
|
+
case 'orchestrator-handle-executor-result':
|
|
197
217
|
result = await handleExecutorResult(args || {});
|
|
198
218
|
break;
|
|
199
|
-
case '
|
|
219
|
+
case 'orchestrator-handle-debugger-result':
|
|
200
220
|
result = await handleDebuggerResult(args || {});
|
|
201
221
|
break;
|
|
202
|
-
case '
|
|
222
|
+
case 'orchestrator-handle-researcher-result':
|
|
203
223
|
result = await handleResearcherResult(args || {});
|
|
204
224
|
break;
|
|
205
|
-
case '
|
|
225
|
+
case 'orchestrator-handle-reviewer-result':
|
|
206
226
|
result = await handleReviewerResult(args || {});
|
|
207
227
|
break;
|
|
208
228
|
default:
|
|
@@ -106,7 +106,7 @@ async function evaluatePreflight(state, basePath) {
|
|
|
106
106
|
return { override: null };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const currentGitHead = getGitHead(basePath);
|
|
109
|
+
const currentGitHead = await getGitHead(basePath);
|
|
110
110
|
if (state.git_head && currentGitHead && state.git_head !== currentGitHead) {
|
|
111
111
|
return {
|
|
112
112
|
override: {
|
|
@@ -386,6 +386,7 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
386
386
|
if (state.current_task) {
|
|
387
387
|
const currentTask = getTaskById(phase, state.current_task);
|
|
388
388
|
if (currentTask?.lifecycle === 'running') {
|
|
389
|
+
const isRetrying = (currentTask.retry_count || 0) > 0;
|
|
389
390
|
const persistError = await persist(basePath, {
|
|
390
391
|
workflow_mode: 'executing_task',
|
|
391
392
|
current_task: currentTask.id,
|
|
@@ -394,7 +395,12 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
394
395
|
if (persistError) return persistError;
|
|
395
396
|
return buildExecutorDispatch(state, phase, currentTask, {
|
|
396
397
|
resumed: true,
|
|
397
|
-
interruption_recovered:
|
|
398
|
+
interruption_recovered: !isRetrying,
|
|
399
|
+
...(isRetrying ? {
|
|
400
|
+
retry_after_failure: true,
|
|
401
|
+
retry_count: currentTask.retry_count,
|
|
402
|
+
last_failure_summary: currentTask.last_failure_summary,
|
|
403
|
+
} : {}),
|
|
398
404
|
});
|
|
399
405
|
}
|
|
400
406
|
}
|
|
@@ -419,11 +425,16 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
419
425
|
|
|
420
426
|
if (selection.mode === 'trigger_review') {
|
|
421
427
|
const current_review = { scope: 'phase', scope_id: phase.id };
|
|
422
|
-
const
|
|
428
|
+
const updates = {
|
|
423
429
|
workflow_mode: 'reviewing_phase',
|
|
424
430
|
current_task: null,
|
|
425
431
|
current_review,
|
|
426
|
-
}
|
|
432
|
+
};
|
|
433
|
+
// Auto-advance phase lifecycle to 'reviewing' if currently 'active'
|
|
434
|
+
if (phase.lifecycle === 'active') {
|
|
435
|
+
updates.phases = [{ id: phase.id, lifecycle: 'reviewing' }];
|
|
436
|
+
}
|
|
437
|
+
const persistError = await persist(basePath, updates);
|
|
427
438
|
if (persistError) return persistError;
|
|
428
439
|
|
|
429
440
|
return {
|
|
@@ -562,6 +573,7 @@ export async function resumeWorkflow({ basePath = process.cwd() } = {}) {
|
|
|
562
573
|
id: task.id,
|
|
563
574
|
level: task.level,
|
|
564
575
|
checkpoint_commit: task.checkpoint_commit || null,
|
|
576
|
+
files_changed: task.files_changed || [],
|
|
565
577
|
})),
|
|
566
578
|
};
|
|
567
579
|
}
|
|
@@ -880,6 +892,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
880
892
|
|
|
881
893
|
const taskPatches = [];
|
|
882
894
|
let doneIncrement = 0;
|
|
895
|
+
let doneDecrement = 0;
|
|
883
896
|
|
|
884
897
|
// Accept tasks
|
|
885
898
|
for (const taskId of (result.accepted_tasks || [])) {
|
|
@@ -896,6 +909,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
896
909
|
const task = getTaskById(phase, taskId);
|
|
897
910
|
if (!task) continue;
|
|
898
911
|
if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
|
|
912
|
+
if (task.lifecycle === 'accepted') doneDecrement += 1;
|
|
899
913
|
taskPatches.push({ id: taskId, lifecycle: 'needs_revalidation', evidence_refs: [] });
|
|
900
914
|
}
|
|
901
915
|
}
|
|
@@ -919,7 +933,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
919
933
|
|
|
920
934
|
const phaseUpdates = {
|
|
921
935
|
id: phase.id,
|
|
922
|
-
done: (phase.done || 0) + doneIncrement,
|
|
936
|
+
done: Math.max(0, (phase.done || 0) + doneIncrement - doneDecrement),
|
|
923
937
|
phase_review: {
|
|
924
938
|
status: reviewStatus,
|
|
925
939
|
...(hasCritical ? { retry_count: (phase.phase_review?.retry_count || 0) + 1 } : {}),
|
package/src/tools/state.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { stat } from 'node:fs/promises';
|
|
5
|
-
import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead } from '../utils.js';
|
|
5
|
+
import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead, isPlainObject } from '../utils.js';
|
|
6
6
|
import {
|
|
7
7
|
CANONICAL_FIELDS,
|
|
8
8
|
TASK_LIFECYCLE,
|
|
@@ -25,10 +25,6 @@ function withStateLock(fn) {
|
|
|
25
25
|
return p;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function isPlainObject(value) {
|
|
29
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
function inferWorkflowModeAfterResearch(state) {
|
|
33
29
|
if (state.current_review?.scope === 'phase') return 'reviewing_phase';
|
|
34
30
|
if (state.current_review?.scope === 'task') return 'reviewing_task';
|
|
@@ -73,7 +69,8 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
const state = createInitialState({ project, phases });
|
|
76
|
-
state.
|
|
72
|
+
if (state.error) return state;
|
|
73
|
+
state.git_head = await getGitHead(basePath);
|
|
77
74
|
|
|
78
75
|
// Create plan.md placeholder (atomic write)
|
|
79
76
|
await writeAtomic(
|
|
@@ -388,7 +385,7 @@ export async function phaseComplete({
|
|
|
388
385
|
|
|
389
386
|
// Update git_head to current commit
|
|
390
387
|
const gsdDir = dirname(statePath);
|
|
391
|
-
state.git_head = getGitHead(dirname(gsdDir));
|
|
388
|
+
state.git_head = await getGitHead(dirname(gsdDir));
|
|
392
389
|
|
|
393
390
|
// Prune evidence from old phases (in-memory to avoid double read/write)
|
|
394
391
|
await _pruneEvidenceFromState(state, state.current_phase, gsdDir);
|
|
@@ -557,6 +554,12 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
|
|
|
557
554
|
return { mode: 'trigger_review' };
|
|
558
555
|
}
|
|
559
556
|
|
|
557
|
+
// All tasks accepted → trigger phase review if not already reviewed
|
|
558
|
+
const allAccepted = phase.todo.length > 0 && phase.todo.every(t => t.lifecycle === 'accepted');
|
|
559
|
+
if (allAccepted && phase.phase_review?.status !== 'accepted') {
|
|
560
|
+
return { mode: 'trigger_review' };
|
|
561
|
+
}
|
|
562
|
+
|
|
560
563
|
const blockedTasks = phase.todo.filter(t => t.lifecycle === 'blocked');
|
|
561
564
|
if (blockedTasks.length > 0) {
|
|
562
565
|
return { mode: 'awaiting_user', blockers: blockedTasks.map(t => ({ id: t.id, reason: t.blocked_reason })) };
|
package/src/tools/verify.js
CHANGED
|
@@ -81,9 +81,10 @@ export async function runAll(cwd = process.cwd()) {
|
|
|
81
81
|
const errResult = { exit_code: -1, summary: 'No package manager detected' };
|
|
82
82
|
return { lint: errResult, typecheck: errResult, test: errResult };
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const [lint, typecheck, test] = await Promise.all([
|
|
85
|
+
runLint(pm, cwd),
|
|
86
|
+
runTypeCheck(cwd),
|
|
87
|
+
runTests(pm, cwd),
|
|
88
|
+
]);
|
|
89
|
+
return { lint, typecheck, test };
|
|
89
90
|
}
|
package/src/utils.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile, rename, mkdir, unlink } from 'node:fs/promises';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { join, dirname, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFileCb);
|
|
5
8
|
|
|
6
9
|
export function getGsdDir(startDir = process.cwd()) {
|
|
7
10
|
let dir = resolve(startDir);
|
|
@@ -23,18 +26,22 @@ export function getStatePath(startDir = process.cwd()) {
|
|
|
23
26
|
return join(gsdDir, 'state.json');
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export function getGitHead(cwd = process.cwd()) {
|
|
29
|
+
export async function getGitHead(cwd = process.cwd()) {
|
|
27
30
|
try {
|
|
28
|
-
|
|
31
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
29
32
|
cwd,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
});
|
|
35
|
+
return stdout.trim();
|
|
33
36
|
} catch {
|
|
34
37
|
return null;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
export function isPlainObject(value) {
|
|
42
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
export async function ensureDir(dirPath) {
|
|
39
46
|
await mkdir(dirPath, { recursive: true });
|
|
40
47
|
}
|
|
@@ -56,18 +63,28 @@ export async function readJson(filePath) {
|
|
|
56
63
|
* Atomically write JSON data (write to .tmp then rename).
|
|
57
64
|
*/
|
|
58
65
|
export async function writeJson(filePath, data) {
|
|
59
|
-
const tmpPath = filePath
|
|
66
|
+
const tmpPath = `${filePath}.${process.pid}-${Date.now()}.tmp`;
|
|
60
67
|
await ensureDir(dirname(filePath));
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
try {
|
|
69
|
+
await writeFile(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
70
|
+
await rename(tmpPath, filePath);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
try { await unlink(tmpPath); } catch {}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
/**
|
|
66
78
|
* Atomically write text content (write to .tmp then rename). [I-3]
|
|
67
79
|
*/
|
|
68
80
|
export async function writeAtomic(filePath, content) {
|
|
69
|
-
const tmpPath = filePath
|
|
81
|
+
const tmpPath = `${filePath}.${process.pid}-${Date.now()}.tmp`;
|
|
70
82
|
await ensureDir(dirname(filePath));
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
try {
|
|
84
|
+
await writeFile(tmpPath, content, 'utf-8');
|
|
85
|
+
await rename(tmpPath, filePath);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
try { await unlink(tmpPath); } catch {}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
73
90
|
}
|
package/uninstall.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Plugin uninstaller for GSD-Lite
|
|
3
3
|
|
|
4
|
-
import { existsSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, rmSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
|
|
9
|
-
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
10
|
-
const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd
|
|
9
|
+
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
10
|
+
const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
|
|
11
11
|
|
|
12
12
|
function log(msg) { console.log(msg); }
|
|
13
13
|
|
|
14
|
+
function atomicWriteSync(filePath, content) {
|
|
15
|
+
const tmp = filePath + `.${process.pid}-${Date.now()}.tmp`;
|
|
16
|
+
writeFileSync(tmp, content);
|
|
17
|
+
renameSync(tmp, filePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
function removeDir(path, label) {
|
|
15
21
|
if (existsSync(path)) {
|
|
16
22
|
rmSync(path, { recursive: true, force: true });
|
|
@@ -28,52 +34,108 @@ export function main() {
|
|
|
28
34
|
removeDir(join(CLAUDE_DIR, 'agents', 'gsd'), 'agents/gsd/');
|
|
29
35
|
removeDir(join(CLAUDE_DIR, 'workflows', 'gsd'), 'workflows/gsd/');
|
|
30
36
|
removeDir(join(CLAUDE_DIR, 'references', 'gsd'), 'references/gsd/');
|
|
31
|
-
removeDir(RUNTIME_DIR, 'gsd
|
|
37
|
+
removeDir(RUNTIME_DIR, 'gsd runtime/');
|
|
38
|
+
removeDir(join(CLAUDE_DIR, 'gsd-lite'), 'legacy gsd-lite runtime/');
|
|
39
|
+
|
|
40
|
+
// Remove hook files (both legacy and current names)
|
|
41
|
+
for (const name of ['context-monitor.js', 'gsd-statusline.cjs', 'gsd-context-monitor.cjs', 'gsd-session-init.cjs']) {
|
|
42
|
+
const hookFile = join(CLAUDE_DIR, 'hooks', name);
|
|
43
|
+
if (existsSync(hookFile)) {
|
|
44
|
+
rmSync(hookFile);
|
|
45
|
+
log(` ✓ Removed hooks/${name}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
32
48
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
// Clean up plugin system directories (from /plugin install)
|
|
50
|
+
removeDir(join(CLAUDE_DIR, 'plugins', 'marketplaces', 'gsd'), 'plugins/marketplaces/gsd/');
|
|
51
|
+
removeDir(join(CLAUDE_DIR, 'plugins', 'cache', 'gsd'), 'plugins/cache/gsd/');
|
|
52
|
+
// Legacy "gsd-lite" plugin directories
|
|
53
|
+
removeDir(join(CLAUDE_DIR, 'plugins', 'marketplaces', 'gsd-lite'), 'plugins/marketplaces/gsd-lite/');
|
|
54
|
+
removeDir(join(CLAUDE_DIR, 'plugins', 'cache', 'gsd-lite'), 'plugins/cache/gsd-lite/');
|
|
55
|
+
|
|
56
|
+
// Deregister from plugin registry files
|
|
57
|
+
const pluginsDir = join(CLAUDE_DIR, 'plugins');
|
|
58
|
+
function removeJsonEntry(filePath, key, label) {
|
|
59
|
+
try {
|
|
60
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
61
|
+
if (key in data) {
|
|
62
|
+
delete data[key];
|
|
63
|
+
atomicWriteSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
64
|
+
log(` ✓ Removed '${key}' from ${label}`);
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
function removeNestedEntry(filePath, parentKey, key, label) {
|
|
69
|
+
try {
|
|
70
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
71
|
+
if (data[parentKey] && key in data[parentKey]) {
|
|
72
|
+
delete data[parentKey][key];
|
|
73
|
+
atomicWriteSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
74
|
+
log(` ✓ Removed '${key}' from ${label}`);
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
for (const name of ['gsd', 'gsd-lite']) {
|
|
79
|
+
removeJsonEntry(join(pluginsDir, 'known_marketplaces.json'), name, 'known_marketplaces.json');
|
|
80
|
+
removeNestedEntry(join(pluginsDir, 'installed_plugins.json'), 'plugins', `${name}@${name}`, 'installed_plugins.json');
|
|
38
81
|
}
|
|
39
82
|
|
|
40
|
-
// Deregister MCP server and
|
|
83
|
+
// Deregister MCP server, hooks, and plugin entries from settings.json
|
|
41
84
|
const settingsPath = join(CLAUDE_DIR, 'settings.json');
|
|
42
85
|
try {
|
|
43
86
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
44
87
|
let changed = false;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
88
|
+
// Remove both current and legacy MCP server + plugin entries
|
|
89
|
+
for (const name of ['gsd', 'gsd-lite']) {
|
|
90
|
+
if (settings.mcpServers?.[name]) {
|
|
91
|
+
delete settings.mcpServers[name];
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
const pluginKey = `${name}@${name}`;
|
|
95
|
+
if (settings.enabledPlugins?.[pluginKey]) {
|
|
96
|
+
delete settings.enabledPlugins[pluginKey];
|
|
97
|
+
changed = true;
|
|
98
|
+
}
|
|
99
|
+
if (settings.extraKnownMarketplaces?.[name]) {
|
|
100
|
+
delete settings.extraKnownMarketplaces[name];
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (settings.extraKnownMarketplaces && Object.keys(settings.extraKnownMarketplaces).length === 0) {
|
|
105
|
+
delete settings.extraKnownMarketplaces;
|
|
48
106
|
}
|
|
49
|
-
// Remove top-level statusLine if GSD's
|
|
50
|
-
if (settings.statusLine?.command?.includes('
|
|
107
|
+
// Remove top-level statusLine if GSD's (match both old and new patterns)
|
|
108
|
+
if (settings.statusLine?.command?.includes('gsd-statusline') ||
|
|
109
|
+
settings.statusLine?.command?.includes('context-monitor.js')) {
|
|
51
110
|
delete settings.statusLine;
|
|
52
111
|
changed = true;
|
|
53
112
|
}
|
|
54
113
|
if (settings.hooks) {
|
|
55
|
-
// Remove legacy StatusLine
|
|
114
|
+
// Remove legacy StatusLine hook entry
|
|
56
115
|
if (typeof settings.hooks.StatusLine === 'string'
|
|
57
|
-
&& settings.hooks.StatusLine.includes('
|
|
116
|
+
&& (settings.hooks.StatusLine.includes('gsd-statusline') ||
|
|
117
|
+
settings.hooks.StatusLine.includes('context-monitor.js'))) {
|
|
58
118
|
delete settings.hooks.StatusLine;
|
|
59
119
|
changed = true;
|
|
60
120
|
}
|
|
61
|
-
// Remove GSD PostToolUse entry from array
|
|
121
|
+
// Remove GSD PostToolUse entry from array (match both old and new patterns)
|
|
62
122
|
if (Array.isArray(settings.hooks.PostToolUse)) {
|
|
63
123
|
const len = settings.hooks.PostToolUse.length;
|
|
64
124
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e =>
|
|
65
|
-
!e.hooks?.some(h => h.command?.includes('context-monitor
|
|
125
|
+
!e.hooks?.some(h => h.command?.includes('gsd-context-monitor') ||
|
|
126
|
+
h.command?.includes('context-monitor.js')));
|
|
66
127
|
if (settings.hooks.PostToolUse.length < len) changed = true;
|
|
67
128
|
if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
|
|
68
129
|
} else if (typeof settings.hooks.PostToolUse === 'string'
|
|
69
|
-
&& settings.hooks.PostToolUse.includes('context-monitor
|
|
130
|
+
&& (settings.hooks.PostToolUse.includes('gsd-context-monitor') ||
|
|
131
|
+
settings.hooks.PostToolUse.includes('context-monitor.js'))) {
|
|
70
132
|
delete settings.hooks.PostToolUse;
|
|
71
133
|
changed = true;
|
|
72
134
|
}
|
|
73
135
|
}
|
|
74
136
|
if (changed) {
|
|
75
|
-
|
|
76
|
-
log(' ✓ MCP server + hooks deregistered from settings.json');
|
|
137
|
+
atomicWriteSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
138
|
+
log(' ✓ MCP server + hooks + plugin entries deregistered from settings.json');
|
|
77
139
|
}
|
|
78
140
|
} catch {}
|
|
79
141
|
|
package/workflows/debugging.md
CHANGED
package/workflows/research.md
CHANGED