get-shit-done-cc 1.9.4 → 1.9.11
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/README.md +45 -6
- package/bin/install.js +832 -122
- package/commands/gsd/help.md +8 -0
- package/commands/gsd/join-discord.md +18 -0
- package/commands/gsd/new-project.md +15 -11
- package/commands/gsd/plan-phase.md +7 -7
- package/commands/gsd/research-phase.md +5 -5
- package/get-shit-done/templates/context.md +0 -8
- package/get-shit-done/templates/state.md +0 -30
- package/get-shit-done/templates/summary.md +5 -28
- package/get-shit-done/templates/user-setup.md +1 -13
- package/get-shit-done/workflows/complete-milestone.md +1 -6
- package/get-shit-done/workflows/diagnose-issues.md +2 -15
- package/get-shit-done/workflows/discuss-phase.md +1 -1
- package/get-shit-done/workflows/execute-phase.md +3 -18
- package/get-shit-done/workflows/execute-plan.md +2 -32
- package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/resume-project.md +3 -11
- package/get-shit-done/workflows/transition.md +1 -9
- package/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -15,6 +15,79 @@ const reset = '\x1b[0m';
|
|
|
15
15
|
// Get version from package.json
|
|
16
16
|
const pkg = require('../package.json');
|
|
17
17
|
|
|
18
|
+
// Parse args
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
21
|
+
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
22
|
+
const hasOpencode = args.includes('--opencode');
|
|
23
|
+
const hasClaude = args.includes('--claude');
|
|
24
|
+
const hasBoth = args.includes('--both');
|
|
25
|
+
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
26
|
+
|
|
27
|
+
// Runtime selection - can be set by flags or interactive prompt
|
|
28
|
+
let selectedRuntimes = [];
|
|
29
|
+
if (hasBoth) {
|
|
30
|
+
selectedRuntimes = ['claude', 'opencode'];
|
|
31
|
+
} else if (hasOpencode) {
|
|
32
|
+
selectedRuntimes = ['opencode'];
|
|
33
|
+
} else if (hasClaude) {
|
|
34
|
+
selectedRuntimes = ['claude'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Helper to get directory name for a runtime (used for local/project installs)
|
|
38
|
+
function getDirName(runtime) {
|
|
39
|
+
return runtime === 'opencode' ? '.opencode' : '.claude';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the global config directory for OpenCode
|
|
44
|
+
* OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
|
|
45
|
+
* Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
|
|
46
|
+
*/
|
|
47
|
+
function getOpencodeGlobalDir() {
|
|
48
|
+
// 1. Explicit OPENCODE_CONFIG_DIR env var
|
|
49
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
50
|
+
return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. OPENCODE_CONFIG env var (use its directory)
|
|
54
|
+
if (process.env.OPENCODE_CONFIG) {
|
|
55
|
+
return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. XDG_CONFIG_HOME/opencode
|
|
59
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
60
|
+
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Default: ~/.config/opencode (XDG default)
|
|
64
|
+
return path.join(os.homedir(), '.config', 'opencode');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the global config directory for a runtime
|
|
69
|
+
* @param {string} runtime - 'claude' or 'opencode'
|
|
70
|
+
* @param {string|null} explicitDir - Explicit directory from --config-dir flag
|
|
71
|
+
*/
|
|
72
|
+
function getGlobalDir(runtime, explicitDir = null) {
|
|
73
|
+
if (runtime === 'opencode') {
|
|
74
|
+
// For OpenCode, --config-dir overrides env vars
|
|
75
|
+
if (explicitDir) {
|
|
76
|
+
return expandTilde(explicitDir);
|
|
77
|
+
}
|
|
78
|
+
return getOpencodeGlobalDir();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
|
82
|
+
if (explicitDir) {
|
|
83
|
+
return expandTilde(explicitDir);
|
|
84
|
+
}
|
|
85
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
86
|
+
return expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
87
|
+
}
|
|
88
|
+
return path.join(os.homedir(), '.claude');
|
|
89
|
+
}
|
|
90
|
+
|
|
18
91
|
const banner = `
|
|
19
92
|
${cyan} ██████╗ ███████╗██████╗
|
|
20
93
|
██╔════╝ ██╔════╝██╔══██╗
|
|
@@ -25,14 +98,9 @@ ${cyan} ██████╗ ███████╗██████╗
|
|
|
25
98
|
|
|
26
99
|
Get Shit Done ${dim}v${pkg.version}${reset}
|
|
27
100
|
A meta-prompting, context engineering and spec-driven
|
|
28
|
-
development system for Claude Code by TÂCHES.
|
|
101
|
+
development system for Claude Code (and opencode) by TÂCHES.
|
|
29
102
|
`;
|
|
30
103
|
|
|
31
|
-
// Parse args
|
|
32
|
-
const args = process.argv.slice(2);
|
|
33
|
-
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
34
|
-
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
35
|
-
|
|
36
104
|
// Parse --config-dir argument
|
|
37
105
|
function parseConfigDirArg() {
|
|
38
106
|
const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
|
|
@@ -68,24 +136,40 @@ if (hasHelp) {
|
|
|
68
136
|
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]
|
|
69
137
|
|
|
70
138
|
${yellow}Options:${reset}
|
|
71
|
-
${cyan}-g, --global${reset} Install globally (to
|
|
72
|
-
${cyan}-l, --local${reset} Install locally (to
|
|
73
|
-
${cyan}
|
|
139
|
+
${cyan}-g, --global${reset} Install globally (to config directory)
|
|
140
|
+
${cyan}-l, --local${reset} Install locally (to current directory)
|
|
141
|
+
${cyan}--claude${reset} Install for Claude Code only
|
|
142
|
+
${cyan}--opencode${reset} Install for OpenCode only
|
|
143
|
+
${cyan}--both${reset} Install for both Claude Code and OpenCode
|
|
144
|
+
${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)
|
|
145
|
+
${cyan}-c, --config-dir <path>${reset} Specify custom config directory
|
|
74
146
|
${cyan}-h, --help${reset} Show this help message
|
|
75
147
|
${cyan}--force-statusline${reset} Replace existing statusline config
|
|
76
148
|
|
|
77
149
|
${yellow}Examples:${reset}
|
|
78
|
-
${dim}#
|
|
79
|
-
npx get-shit-done-cc
|
|
150
|
+
${dim}# Interactive install (prompts for runtime and location)${reset}
|
|
151
|
+
npx get-shit-done-cc
|
|
80
152
|
|
|
81
|
-
${dim}# Install
|
|
82
|
-
npx get-shit-done-cc --
|
|
153
|
+
${dim}# Install for Claude Code globally${reset}
|
|
154
|
+
npx get-shit-done-cc --claude --global
|
|
83
155
|
|
|
84
|
-
${dim}#
|
|
85
|
-
|
|
156
|
+
${dim}# Install for OpenCode globally${reset}
|
|
157
|
+
npx get-shit-done-cc --opencode --global
|
|
158
|
+
|
|
159
|
+
${dim}# Install for both runtimes globally${reset}
|
|
160
|
+
npx get-shit-done-cc --both --global
|
|
161
|
+
|
|
162
|
+
${dim}# Install to custom config directory${reset}
|
|
163
|
+
npx get-shit-done-cc --claude --global --config-dir ~/.claude-bc
|
|
86
164
|
|
|
87
165
|
${dim}# Install to current project only${reset}
|
|
88
|
-
npx get-shit-done-cc --local
|
|
166
|
+
npx get-shit-done-cc --claude --local
|
|
167
|
+
|
|
168
|
+
${dim}# Uninstall GSD from Claude Code globally${reset}
|
|
169
|
+
npx get-shit-done-cc --claude --global --uninstall
|
|
170
|
+
|
|
171
|
+
${dim}# Uninstall GSD from current project${reset}
|
|
172
|
+
npx get-shit-done-cc --claude --local --uninstall
|
|
89
173
|
|
|
90
174
|
${yellow}Notes:${reset}
|
|
91
175
|
The --config-dir option is useful when you have multiple Claude Code
|
|
@@ -106,7 +190,17 @@ function expandTilde(filePath) {
|
|
|
106
190
|
}
|
|
107
191
|
|
|
108
192
|
/**
|
|
109
|
-
*
|
|
193
|
+
* Build a hook command path using forward slashes for cross-platform compatibility.
|
|
194
|
+
* On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
|
|
195
|
+
*/
|
|
196
|
+
function buildHookCommand(claudeDir, hookName) {
|
|
197
|
+
// Use forward slashes for Node.js compatibility on all platforms
|
|
198
|
+
const hooksPath = claudeDir.replace(/\\/g, '/') + '/hooks/' + hookName;
|
|
199
|
+
return `node "${hooksPath}"`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Read and parse settings.json, returning empty object if it doesn't exist
|
|
110
204
|
*/
|
|
111
205
|
function readSettings(settingsPath) {
|
|
112
206
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -126,11 +220,226 @@ function writeSettings(settingsPath, settings) {
|
|
|
126
220
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
127
221
|
}
|
|
128
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Convert Claude Code frontmatter to opencode format
|
|
225
|
+
* - Converts 'allowed-tools:' array to 'permission:' object
|
|
226
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
227
|
+
* @returns {string} - Content with converted frontmatter
|
|
228
|
+
*/
|
|
229
|
+
// Color name to hex mapping for opencode compatibility
|
|
230
|
+
const colorNameToHex = {
|
|
231
|
+
cyan: '#00FFFF',
|
|
232
|
+
red: '#FF0000',
|
|
233
|
+
green: '#00FF00',
|
|
234
|
+
blue: '#0000FF',
|
|
235
|
+
yellow: '#FFFF00',
|
|
236
|
+
magenta: '#FF00FF',
|
|
237
|
+
orange: '#FFA500',
|
|
238
|
+
purple: '#800080',
|
|
239
|
+
pink: '#FFC0CB',
|
|
240
|
+
white: '#FFFFFF',
|
|
241
|
+
black: '#000000',
|
|
242
|
+
gray: '#808080',
|
|
243
|
+
grey: '#808080',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Tool name mapping from Claude Code to OpenCode
|
|
247
|
+
// OpenCode uses lowercase tool names; special mappings for renamed tools
|
|
248
|
+
const claudeToOpencodeTools = {
|
|
249
|
+
AskUserQuestion: 'question',
|
|
250
|
+
SlashCommand: 'skill',
|
|
251
|
+
TodoWrite: 'todowrite',
|
|
252
|
+
WebFetch: 'webfetch',
|
|
253
|
+
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Convert a Claude Code tool name to OpenCode format
|
|
258
|
+
* - Applies special mappings (AskUserQuestion -> question, etc.)
|
|
259
|
+
* - Converts to lowercase (except MCP tools which keep their format)
|
|
260
|
+
*/
|
|
261
|
+
function convertToolName(claudeTool) {
|
|
262
|
+
// Check for special mapping first
|
|
263
|
+
if (claudeToOpencodeTools[claudeTool]) {
|
|
264
|
+
return claudeToOpencodeTools[claudeTool];
|
|
265
|
+
}
|
|
266
|
+
// MCP tools (mcp__*) keep their format
|
|
267
|
+
if (claudeTool.startsWith('mcp__')) {
|
|
268
|
+
return claudeTool;
|
|
269
|
+
}
|
|
270
|
+
// Default: convert to lowercase
|
|
271
|
+
return claudeTool.toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function convertClaudeToOpencodeFrontmatter(content) {
|
|
275
|
+
// Replace tool name references in content (applies to all files)
|
|
276
|
+
let convertedContent = content;
|
|
277
|
+
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
278
|
+
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
279
|
+
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
280
|
+
// Replace /gsd:command with /gsd-command for opencode (flat command structure)
|
|
281
|
+
convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
|
|
282
|
+
// Replace ~/.claude with ~/.config/opencode (OpenCode's correct config location)
|
|
283
|
+
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
|
|
284
|
+
|
|
285
|
+
// Check if content has frontmatter
|
|
286
|
+
if (!convertedContent.startsWith('---')) {
|
|
287
|
+
return convertedContent;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Find the end of frontmatter
|
|
291
|
+
const endIndex = convertedContent.indexOf('---', 3);
|
|
292
|
+
if (endIndex === -1) {
|
|
293
|
+
return convertedContent;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const frontmatter = convertedContent.substring(3, endIndex).trim();
|
|
297
|
+
const body = convertedContent.substring(endIndex + 3);
|
|
298
|
+
|
|
299
|
+
// Parse frontmatter line by line (simple YAML parsing)
|
|
300
|
+
const lines = frontmatter.split('\n');
|
|
301
|
+
const newLines = [];
|
|
302
|
+
let inAllowedTools = false;
|
|
303
|
+
const allowedTools = [];
|
|
304
|
+
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
|
|
308
|
+
// Detect start of allowed-tools array
|
|
309
|
+
if (trimmed.startsWith('allowed-tools:')) {
|
|
310
|
+
inAllowedTools = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Detect inline tools: field (comma-separated string)
|
|
315
|
+
if (trimmed.startsWith('tools:')) {
|
|
316
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
317
|
+
if (toolsValue) {
|
|
318
|
+
// Parse comma-separated tools
|
|
319
|
+
const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
|
|
320
|
+
allowedTools.push(...tools);
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Remove name: field - opencode uses filename for command name
|
|
326
|
+
if (trimmed.startsWith('name:')) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Convert color names to hex for opencode
|
|
331
|
+
if (trimmed.startsWith('color:')) {
|
|
332
|
+
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
333
|
+
const hexColor = colorNameToHex[colorValue];
|
|
334
|
+
if (hexColor) {
|
|
335
|
+
newLines.push(`color: "${hexColor}"`);
|
|
336
|
+
} else if (colorValue.startsWith('#')) {
|
|
337
|
+
// Already hex, keep as is
|
|
338
|
+
newLines.push(line);
|
|
339
|
+
}
|
|
340
|
+
// Skip unknown color names
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Collect allowed-tools items
|
|
345
|
+
if (inAllowedTools) {
|
|
346
|
+
if (trimmed.startsWith('- ')) {
|
|
347
|
+
allowedTools.push(trimmed.substring(2).trim());
|
|
348
|
+
continue;
|
|
349
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
350
|
+
// End of array, new field started
|
|
351
|
+
inAllowedTools = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Keep other fields (including name: which opencode ignores)
|
|
356
|
+
if (!inAllowedTools) {
|
|
357
|
+
newLines.push(line);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add tools object if we had allowed-tools or tools
|
|
362
|
+
if (allowedTools.length > 0) {
|
|
363
|
+
newLines.push('tools:');
|
|
364
|
+
for (const tool of allowedTools) {
|
|
365
|
+
newLines.push(` ${convertToolName(tool)}: true`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Rebuild frontmatter (body already has tool names converted)
|
|
370
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
371
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Copy commands to a flat structure for OpenCode
|
|
376
|
+
* OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
|
|
377
|
+
* Source structure: commands/gsd/help.md
|
|
378
|
+
*
|
|
379
|
+
* @param {string} srcDir - Source directory (e.g., commands/gsd/)
|
|
380
|
+
* @param {string} destDir - Destination directory (e.g., command/)
|
|
381
|
+
* @param {string} prefix - Prefix for filenames (e.g., 'gsd')
|
|
382
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
383
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
384
|
+
*/
|
|
385
|
+
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
386
|
+
if (!fs.existsSync(srcDir)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Remove old gsd-*.md files before copying new ones
|
|
391
|
+
if (fs.existsSync(destDir)) {
|
|
392
|
+
for (const file of fs.readdirSync(destDir)) {
|
|
393
|
+
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
394
|
+
fs.unlinkSync(path.join(destDir, file));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
402
|
+
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
405
|
+
|
|
406
|
+
if (entry.isDirectory()) {
|
|
407
|
+
// Recurse into subdirectories, adding to prefix
|
|
408
|
+
// e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md
|
|
409
|
+
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
410
|
+
} else if (entry.name.endsWith('.md')) {
|
|
411
|
+
// Flatten: help.md -> gsd-help.md
|
|
412
|
+
const baseName = entry.name.replace('.md', '');
|
|
413
|
+
const destName = `${prefix}-${baseName}.md`;
|
|
414
|
+
const destPath = path.join(destDir, destName);
|
|
415
|
+
|
|
416
|
+
// Read, transform, and write
|
|
417
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
418
|
+
// Replace path references
|
|
419
|
+
const claudeDirRegex = /~\/\.claude\//g;
|
|
420
|
+
const opencodeDirRegex = /~\/\.opencode\//g;
|
|
421
|
+
content = content.replace(claudeDirRegex, pathPrefix);
|
|
422
|
+
content = content.replace(opencodeDirRegex, pathPrefix);
|
|
423
|
+
// Convert frontmatter for opencode compatibility
|
|
424
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
425
|
+
|
|
426
|
+
fs.writeFileSync(destPath, content);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
129
431
|
/**
|
|
130
432
|
* Recursively copy directory, replacing paths in .md files
|
|
131
433
|
* Deletes existing destDir first to remove orphaned files from previous versions
|
|
434
|
+
* @param {string} srcDir - Source directory
|
|
435
|
+
* @param {string} destDir - Destination directory
|
|
436
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
437
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
132
438
|
*/
|
|
133
|
-
function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
|
|
439
|
+
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
|
|
440
|
+
const isOpencode = runtime === 'opencode';
|
|
441
|
+
const dirName = getDirName(runtime);
|
|
442
|
+
|
|
134
443
|
// Clean install: remove existing destination to prevent orphaned files
|
|
135
444
|
if (fs.existsSync(destDir)) {
|
|
136
445
|
fs.rmSync(destDir, { recursive: true });
|
|
@@ -144,11 +453,16 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
|
|
|
144
453
|
const destPath = path.join(destDir, entry.name);
|
|
145
454
|
|
|
146
455
|
if (entry.isDirectory()) {
|
|
147
|
-
copyWithPathReplacement(srcPath, destPath, pathPrefix);
|
|
456
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime);
|
|
148
457
|
} else if (entry.name.endsWith('.md')) {
|
|
149
|
-
// Replace ~/.claude/ with the appropriate prefix in
|
|
458
|
+
// Replace ~/.claude/ with the appropriate prefix in Markdown files
|
|
150
459
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
151
|
-
|
|
460
|
+
const claudeDirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
|
|
461
|
+
content = content.replace(claudeDirRegex, pathPrefix);
|
|
462
|
+
// Convert frontmatter for opencode compatibility
|
|
463
|
+
if (isOpencode) {
|
|
464
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
465
|
+
}
|
|
152
466
|
fs.writeFileSync(destPath, content);
|
|
153
467
|
} else {
|
|
154
468
|
fs.copyFileSync(srcPath, destPath);
|
|
@@ -219,6 +533,267 @@ function cleanupOrphanedHooks(settings) {
|
|
|
219
533
|
return settings;
|
|
220
534
|
}
|
|
221
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Uninstall GSD from the specified directory for a specific runtime
|
|
538
|
+
* Removes only GSD-specific files/directories, preserves user content
|
|
539
|
+
* @param {boolean} isGlobal - Whether to uninstall from global or local
|
|
540
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
541
|
+
*/
|
|
542
|
+
function uninstall(isGlobal, runtime = 'claude') {
|
|
543
|
+
const isOpencode = runtime === 'opencode';
|
|
544
|
+
const dirName = getDirName(runtime);
|
|
545
|
+
|
|
546
|
+
// Get the target directory based on runtime and install type
|
|
547
|
+
const targetDir = isGlobal
|
|
548
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
549
|
+
: path.join(process.cwd(), dirName);
|
|
550
|
+
|
|
551
|
+
const locationLabel = isGlobal
|
|
552
|
+
? targetDir.replace(os.homedir(), '~')
|
|
553
|
+
: targetDir.replace(process.cwd(), '.');
|
|
554
|
+
|
|
555
|
+
const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
556
|
+
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
557
|
+
|
|
558
|
+
// Check if target directory exists
|
|
559
|
+
if (!fs.existsSync(targetDir)) {
|
|
560
|
+
console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`);
|
|
561
|
+
console.log(` Nothing to uninstall.\n`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let removedCount = 0;
|
|
566
|
+
|
|
567
|
+
// 1. Remove GSD commands directory
|
|
568
|
+
if (isOpencode) {
|
|
569
|
+
// OpenCode: remove command/gsd-*.md files
|
|
570
|
+
const commandDir = path.join(targetDir, 'command');
|
|
571
|
+
if (fs.existsSync(commandDir)) {
|
|
572
|
+
const files = fs.readdirSync(commandDir);
|
|
573
|
+
for (const file of files) {
|
|
574
|
+
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
575
|
+
fs.unlinkSync(path.join(commandDir, file));
|
|
576
|
+
removedCount++;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
console.log(` ${green}✓${reset} Removed GSD commands from command/`);
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
// Claude Code: remove commands/gsd/ directory
|
|
583
|
+
const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
|
584
|
+
if (fs.existsSync(gsdCommandsDir)) {
|
|
585
|
+
fs.rmSync(gsdCommandsDir, { recursive: true });
|
|
586
|
+
removedCount++;
|
|
587
|
+
console.log(` ${green}✓${reset} Removed commands/gsd/`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 2. Remove get-shit-done directory
|
|
592
|
+
const gsdDir = path.join(targetDir, 'get-shit-done');
|
|
593
|
+
if (fs.existsSync(gsdDir)) {
|
|
594
|
+
fs.rmSync(gsdDir, { recursive: true });
|
|
595
|
+
removedCount++;
|
|
596
|
+
console.log(` ${green}✓${reset} Removed get-shit-done/`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 3. Remove GSD agents (gsd-*.md files only)
|
|
600
|
+
const agentsDir = path.join(targetDir, 'agents');
|
|
601
|
+
if (fs.existsSync(agentsDir)) {
|
|
602
|
+
const files = fs.readdirSync(agentsDir);
|
|
603
|
+
let agentCount = 0;
|
|
604
|
+
for (const file of files) {
|
|
605
|
+
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
606
|
+
fs.unlinkSync(path.join(agentsDir, file));
|
|
607
|
+
agentCount++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (agentCount > 0) {
|
|
611
|
+
removedCount++;
|
|
612
|
+
console.log(` ${green}✓${reset} Removed ${agentCount} GSD agents`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 4. Remove GSD hooks
|
|
617
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
618
|
+
if (fs.existsSync(hooksDir)) {
|
|
619
|
+
const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh'];
|
|
620
|
+
let hookCount = 0;
|
|
621
|
+
for (const hook of gsdHooks) {
|
|
622
|
+
const hookPath = path.join(hooksDir, hook);
|
|
623
|
+
if (fs.existsSync(hookPath)) {
|
|
624
|
+
fs.unlinkSync(hookPath);
|
|
625
|
+
hookCount++;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (hookCount > 0) {
|
|
629
|
+
removedCount++;
|
|
630
|
+
console.log(` ${green}✓${reset} Removed ${hookCount} GSD hooks`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 5. Clean up settings.json (remove GSD hooks and statusline)
|
|
635
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
636
|
+
if (fs.existsSync(settingsPath)) {
|
|
637
|
+
let settings = readSettings(settingsPath);
|
|
638
|
+
let settingsModified = false;
|
|
639
|
+
|
|
640
|
+
// Remove GSD statusline if it references our hook
|
|
641
|
+
if (settings.statusLine && settings.statusLine.command &&
|
|
642
|
+
settings.statusLine.command.includes('gsd-statusline')) {
|
|
643
|
+
delete settings.statusLine;
|
|
644
|
+
settingsModified = true;
|
|
645
|
+
console.log(` ${green}✓${reset} Removed GSD statusline from settings`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Remove GSD hooks from SessionStart
|
|
649
|
+
if (settings.hooks && settings.hooks.SessionStart) {
|
|
650
|
+
const before = settings.hooks.SessionStart.length;
|
|
651
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
|
|
652
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
653
|
+
// Filter out GSD hooks
|
|
654
|
+
const hasGsdHook = entry.hooks.some(h =>
|
|
655
|
+
h.command && (h.command.includes('gsd-check-update') || h.command.includes('gsd-statusline'))
|
|
656
|
+
);
|
|
657
|
+
return !hasGsdHook;
|
|
658
|
+
}
|
|
659
|
+
return true;
|
|
660
|
+
});
|
|
661
|
+
if (settings.hooks.SessionStart.length < before) {
|
|
662
|
+
settingsModified = true;
|
|
663
|
+
console.log(` ${green}✓${reset} Removed GSD hooks from settings`);
|
|
664
|
+
}
|
|
665
|
+
// Clean up empty array
|
|
666
|
+
if (settings.hooks.SessionStart.length === 0) {
|
|
667
|
+
delete settings.hooks.SessionStart;
|
|
668
|
+
}
|
|
669
|
+
// Clean up empty hooks object
|
|
670
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
671
|
+
delete settings.hooks;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (settingsModified) {
|
|
676
|
+
writeSettings(settingsPath, settings);
|
|
677
|
+
removedCount++;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 6. For OpenCode, clean up permissions from opencode.json
|
|
682
|
+
if (isOpencode) {
|
|
683
|
+
const opencodeConfigDir = getOpencodeGlobalDir();
|
|
684
|
+
const configPath = path.join(opencodeConfigDir, 'opencode.json');
|
|
685
|
+
if (fs.existsSync(configPath)) {
|
|
686
|
+
try {
|
|
687
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
688
|
+
let modified = false;
|
|
689
|
+
|
|
690
|
+
// Remove GSD permission entries
|
|
691
|
+
if (config.permission) {
|
|
692
|
+
for (const permType of ['read', 'external_directory']) {
|
|
693
|
+
if (config.permission[permType]) {
|
|
694
|
+
const keys = Object.keys(config.permission[permType]);
|
|
695
|
+
for (const key of keys) {
|
|
696
|
+
if (key.includes('get-shit-done')) {
|
|
697
|
+
delete config.permission[permType][key];
|
|
698
|
+
modified = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Clean up empty objects
|
|
702
|
+
if (Object.keys(config.permission[permType]).length === 0) {
|
|
703
|
+
delete config.permission[permType];
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (Object.keys(config.permission).length === 0) {
|
|
708
|
+
delete config.permission;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (modified) {
|
|
713
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
714
|
+
removedCount++;
|
|
715
|
+
console.log(` ${green}✓${reset} Removed GSD permissions from opencode.json`);
|
|
716
|
+
}
|
|
717
|
+
} catch (e) {
|
|
718
|
+
// Ignore JSON parse errors
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (removedCount === 0) {
|
|
724
|
+
console.log(` ${yellow}⚠${reset} No GSD files found to remove.`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
console.log(`
|
|
728
|
+
${green}Done!${reset} GSD has been uninstalled from ${runtimeLabel}.
|
|
729
|
+
Your other files and settings have been preserved.
|
|
730
|
+
`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Configure OpenCode permissions to allow reading GSD reference docs
|
|
735
|
+
* This prevents permission prompts when GSD accesses the get-shit-done directory
|
|
736
|
+
*/
|
|
737
|
+
function configureOpencodePermissions() {
|
|
738
|
+
// OpenCode config file is at ~/.config/opencode/opencode.json
|
|
739
|
+
const opencodeConfigDir = getOpencodeGlobalDir();
|
|
740
|
+
const configPath = path.join(opencodeConfigDir, 'opencode.json');
|
|
741
|
+
|
|
742
|
+
// Ensure config directory exists
|
|
743
|
+
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
744
|
+
|
|
745
|
+
// Read existing config or create empty object
|
|
746
|
+
let config = {};
|
|
747
|
+
if (fs.existsSync(configPath)) {
|
|
748
|
+
try {
|
|
749
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
750
|
+
} catch (e) {
|
|
751
|
+
// Invalid JSON - start fresh but warn user
|
|
752
|
+
console.log(` ${yellow}⚠${reset} opencode.json had invalid JSON, recreating`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Ensure permission structure exists
|
|
757
|
+
if (!config.permission) {
|
|
758
|
+
config.permission = {};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Build the GSD path using the actual config directory
|
|
762
|
+
// Use ~ shorthand if it's in the default location, otherwise use full path
|
|
763
|
+
const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
|
|
764
|
+
const gsdPath = opencodeConfigDir === defaultConfigDir
|
|
765
|
+
? '~/.config/opencode/get-shit-done/*'
|
|
766
|
+
: `${opencodeConfigDir}/get-shit-done/*`;
|
|
767
|
+
|
|
768
|
+
let modified = false;
|
|
769
|
+
|
|
770
|
+
// Configure read permission
|
|
771
|
+
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
772
|
+
config.permission.read = {};
|
|
773
|
+
}
|
|
774
|
+
if (config.permission.read[gsdPath] !== 'allow') {
|
|
775
|
+
config.permission.read[gsdPath] = 'allow';
|
|
776
|
+
modified = true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Configure external_directory permission (the safety guard for paths outside project)
|
|
780
|
+
if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
|
|
781
|
+
config.permission.external_directory = {};
|
|
782
|
+
}
|
|
783
|
+
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
|
784
|
+
config.permission.external_directory[gsdPath] = 'allow';
|
|
785
|
+
modified = true;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (!modified) {
|
|
789
|
+
return; // Already configured
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Write config back
|
|
793
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
794
|
+
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
795
|
+
}
|
|
796
|
+
|
|
222
797
|
/**
|
|
223
798
|
* Verify a directory exists and contains files
|
|
224
799
|
*/
|
|
@@ -252,64 +827,86 @@ function verifyFileInstalled(filePath, description) {
|
|
|
252
827
|
}
|
|
253
828
|
|
|
254
829
|
/**
|
|
255
|
-
* Install to the specified directory
|
|
830
|
+
* Install to the specified directory for a specific runtime
|
|
831
|
+
* @param {boolean} isGlobal - Whether to install globally or locally
|
|
832
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
256
833
|
*/
|
|
257
|
-
function install(isGlobal) {
|
|
834
|
+
function install(isGlobal, runtime = 'claude') {
|
|
835
|
+
const isOpencode = runtime === 'opencode';
|
|
836
|
+
const dirName = getDirName(runtime); // .opencode or .claude (for local installs)
|
|
258
837
|
const src = path.join(__dirname, '..');
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
: path.join(process.cwd(), '.claude');
|
|
838
|
+
|
|
839
|
+
// Get the target directory based on runtime and install type
|
|
840
|
+
const targetDir = isGlobal
|
|
841
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
842
|
+
: path.join(process.cwd(), dirName);
|
|
265
843
|
|
|
266
844
|
const locationLabel = isGlobal
|
|
267
|
-
?
|
|
268
|
-
:
|
|
845
|
+
? targetDir.replace(os.homedir(), '~')
|
|
846
|
+
: targetDir.replace(process.cwd(), '.');
|
|
269
847
|
|
|
270
|
-
// Path prefix for file references
|
|
271
|
-
//
|
|
848
|
+
// Path prefix for file references in markdown content
|
|
849
|
+
// For global installs: use full path (necessary when config dir is customized)
|
|
850
|
+
// For local installs: use relative ./.opencode/ or ./.claude/
|
|
272
851
|
const pathPrefix = isGlobal
|
|
273
|
-
?
|
|
274
|
-
:
|
|
852
|
+
? `${targetDir}/`
|
|
853
|
+
: `./${dirName}/`;
|
|
275
854
|
|
|
276
|
-
|
|
855
|
+
const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
856
|
+
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
277
857
|
|
|
278
858
|
// Track installation failures
|
|
279
859
|
const failures = [];
|
|
280
860
|
|
|
281
861
|
// Clean up orphaned files from previous versions
|
|
282
|
-
cleanupOrphanedFiles(
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
862
|
+
cleanupOrphanedFiles(targetDir);
|
|
863
|
+
|
|
864
|
+
// OpenCode uses 'command/' (singular) with flat structure: command/gsd-help.md
|
|
865
|
+
// Claude Code uses 'commands/' (plural) with nested structure: commands/gsd/help.md
|
|
866
|
+
if (isOpencode) {
|
|
867
|
+
// OpenCode: flat structure in command/ directory
|
|
868
|
+
const commandDir = path.join(targetDir, 'command');
|
|
869
|
+
fs.mkdirSync(commandDir, { recursive: true });
|
|
870
|
+
|
|
871
|
+
// Copy commands/gsd/*.md as command/gsd-*.md (flatten structure)
|
|
872
|
+
const gsdSrc = path.join(src, 'commands', 'gsd');
|
|
873
|
+
copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime);
|
|
874
|
+
if (verifyInstalled(commandDir, 'command/gsd-*')) {
|
|
875
|
+
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsd-')).length;
|
|
876
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
|
|
877
|
+
} else {
|
|
878
|
+
failures.push('command/gsd-*');
|
|
879
|
+
}
|
|
294
880
|
} else {
|
|
295
|
-
|
|
881
|
+
// Claude Code: nested structure in commands/ directory
|
|
882
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
883
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
884
|
+
|
|
885
|
+
const gsdSrc = path.join(src, 'commands', 'gsd');
|
|
886
|
+
const gsdDest = path.join(commandsDir, 'gsd');
|
|
887
|
+
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime);
|
|
888
|
+
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
|
889
|
+
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
|
890
|
+
} else {
|
|
891
|
+
failures.push('commands/gsd');
|
|
892
|
+
}
|
|
296
893
|
}
|
|
297
894
|
|
|
298
895
|
// Copy get-shit-done skill with path replacement
|
|
299
896
|
const skillSrc = path.join(src, 'get-shit-done');
|
|
300
|
-
const skillDest = path.join(
|
|
301
|
-
copyWithPathReplacement(skillSrc, skillDest, pathPrefix);
|
|
897
|
+
const skillDest = path.join(targetDir, 'get-shit-done');
|
|
898
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime);
|
|
302
899
|
if (verifyInstalled(skillDest, 'get-shit-done')) {
|
|
303
900
|
console.log(` ${green}✓${reset} Installed get-shit-done`);
|
|
304
901
|
} else {
|
|
305
902
|
failures.push('get-shit-done');
|
|
306
903
|
}
|
|
307
904
|
|
|
308
|
-
// Copy agents to
|
|
905
|
+
// Copy agents to agents directory (subagents must be at root level)
|
|
309
906
|
// Only delete gsd-*.md files to preserve user's custom agents
|
|
310
907
|
const agentsSrc = path.join(src, 'agents');
|
|
311
908
|
if (fs.existsSync(agentsSrc)) {
|
|
312
|
-
const agentsDest = path.join(
|
|
909
|
+
const agentsDest = path.join(targetDir, 'agents');
|
|
313
910
|
fs.mkdirSync(agentsDest, { recursive: true });
|
|
314
911
|
|
|
315
912
|
// Remove old GSD agents (gsd-*.md) before copying new ones
|
|
@@ -326,7 +923,12 @@ function install(isGlobal) {
|
|
|
326
923
|
for (const entry of agentEntries) {
|
|
327
924
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
328
925
|
let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
|
|
329
|
-
|
|
926
|
+
const dirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
|
|
927
|
+
content = content.replace(dirRegex, pathPrefix);
|
|
928
|
+
// Convert frontmatter for opencode compatibility
|
|
929
|
+
if (isOpencode) {
|
|
930
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
931
|
+
}
|
|
330
932
|
fs.writeFileSync(path.join(agentsDest, entry.name), content);
|
|
331
933
|
}
|
|
332
934
|
}
|
|
@@ -339,7 +941,7 @@ function install(isGlobal) {
|
|
|
339
941
|
|
|
340
942
|
// Copy CHANGELOG.md
|
|
341
943
|
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
342
|
-
const changelogDest = path.join(
|
|
944
|
+
const changelogDest = path.join(targetDir, 'get-shit-done', 'CHANGELOG.md');
|
|
343
945
|
if (fs.existsSync(changelogSrc)) {
|
|
344
946
|
fs.copyFileSync(changelogSrc, changelogDest);
|
|
345
947
|
if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
|
|
@@ -350,7 +952,7 @@ function install(isGlobal) {
|
|
|
350
952
|
}
|
|
351
953
|
|
|
352
954
|
// Write VERSION file for whats-new command
|
|
353
|
-
const versionDest = path.join(
|
|
955
|
+
const versionDest = path.join(targetDir, 'get-shit-done', 'VERSION');
|
|
354
956
|
fs.writeFileSync(versionDest, pkg.version);
|
|
355
957
|
if (verifyFileInstalled(versionDest, 'VERSION')) {
|
|
356
958
|
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
@@ -361,7 +963,7 @@ function install(isGlobal) {
|
|
|
361
963
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
362
964
|
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
363
965
|
if (fs.existsSync(hooksSrc)) {
|
|
364
|
-
const hooksDest = path.join(
|
|
966
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
365
967
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
366
968
|
const hookEntries = fs.readdirSync(hooksSrc);
|
|
367
969
|
for (const entry of hookEntries) {
|
|
@@ -387,48 +989,57 @@ function install(isGlobal) {
|
|
|
387
989
|
}
|
|
388
990
|
|
|
389
991
|
// Configure statusline and hooks in settings.json
|
|
390
|
-
const settingsPath = path.join(
|
|
992
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
391
993
|
const settings = cleanupOrphanedHooks(readSettings(settingsPath));
|
|
392
994
|
const statuslineCommand = isGlobal
|
|
393
|
-
? '
|
|
394
|
-
: 'node
|
|
995
|
+
? buildHookCommand(targetDir, 'gsd-statusline.js')
|
|
996
|
+
: 'node ' + dirName + '/hooks/gsd-statusline.js';
|
|
395
997
|
const updateCheckCommand = isGlobal
|
|
396
|
-
? '
|
|
397
|
-
: 'node
|
|
998
|
+
? buildHookCommand(targetDir, 'gsd-check-update.js')
|
|
999
|
+
: 'node ' + dirName + '/hooks/gsd-check-update.js';
|
|
398
1000
|
|
|
399
|
-
// Configure SessionStart hook for update checking
|
|
400
|
-
if (!
|
|
401
|
-
settings.hooks
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
settings.hooks.SessionStart
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Check if GSD update hook already exists
|
|
408
|
-
const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
|
|
409
|
-
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update'))
|
|
410
|
-
);
|
|
1001
|
+
// Configure SessionStart hook for update checking (skip for opencode - different hook system)
|
|
1002
|
+
if (!isOpencode) {
|
|
1003
|
+
if (!settings.hooks) {
|
|
1004
|
+
settings.hooks = {};
|
|
1005
|
+
}
|
|
1006
|
+
if (!settings.hooks.SessionStart) {
|
|
1007
|
+
settings.hooks.SessionStart = [];
|
|
1008
|
+
}
|
|
411
1009
|
|
|
412
|
-
|
|
413
|
-
settings.hooks.SessionStart.
|
|
414
|
-
hooks
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1010
|
+
// Check if GSD update hook already exists
|
|
1011
|
+
const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
|
|
1012
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update'))
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
if (!hasGsdUpdateHook) {
|
|
1016
|
+
settings.hooks.SessionStart.push({
|
|
1017
|
+
hooks: [
|
|
1018
|
+
{
|
|
1019
|
+
type: 'command',
|
|
1020
|
+
command: updateCheckCommand
|
|
1021
|
+
}
|
|
1022
|
+
]
|
|
1023
|
+
});
|
|
1024
|
+
console.log(` ${green}✓${reset} Configured update check hook`);
|
|
1025
|
+
}
|
|
422
1026
|
}
|
|
423
1027
|
|
|
424
|
-
return { settingsPath, settings, statuslineCommand };
|
|
1028
|
+
return { settingsPath, settings, statuslineCommand, runtime };
|
|
425
1029
|
}
|
|
426
1030
|
|
|
427
1031
|
/**
|
|
428
1032
|
* Apply statusline config, then print completion message
|
|
1033
|
+
* @param {string} settingsPath - Path to settings.json
|
|
1034
|
+
* @param {object} settings - Settings object
|
|
1035
|
+
* @param {string} statuslineCommand - Statusline command
|
|
1036
|
+
* @param {boolean} shouldInstallStatusline - Whether to install statusline
|
|
1037
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
429
1038
|
*/
|
|
430
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline) {
|
|
431
|
-
|
|
1039
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude') {
|
|
1040
|
+
const isOpencode = runtime === 'opencode';
|
|
1041
|
+
|
|
1042
|
+
if (shouldInstallStatusline && !isOpencode) {
|
|
432
1043
|
settings.statusLine = {
|
|
433
1044
|
type: 'command',
|
|
434
1045
|
command: statuslineCommand
|
|
@@ -439,8 +1050,17 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
439
1050
|
// Always write settings (hooks were already configured in install())
|
|
440
1051
|
writeSettings(settingsPath, settings);
|
|
441
1052
|
|
|
1053
|
+
// Configure OpenCode permissions if needed
|
|
1054
|
+
if (isOpencode) {
|
|
1055
|
+
configureOpencodePermissions();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const program = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
1059
|
+
const command = isOpencode ? '/gsd-help' : '/gsd:help';
|
|
442
1060
|
console.log(`
|
|
443
|
-
${green}Done!${reset} Launch
|
|
1061
|
+
${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
|
|
1062
|
+
|
|
1063
|
+
${cyan}Join the community:${reset} https://discord.gg/5JJgD5svVS
|
|
444
1064
|
`);
|
|
445
1065
|
}
|
|
446
1066
|
|
|
@@ -500,18 +1120,57 @@ function handleStatusline(settings, isInteractive, callback) {
|
|
|
500
1120
|
});
|
|
501
1121
|
}
|
|
502
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Prompt for runtime selection (Claude Code / OpenCode / Both)
|
|
1125
|
+
* @param {function} callback - Called with array of selected runtimes
|
|
1126
|
+
*/
|
|
1127
|
+
function promptRuntime(callback) {
|
|
1128
|
+
const rl = readline.createInterface({
|
|
1129
|
+
input: process.stdin,
|
|
1130
|
+
output: process.stdout
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
let answered = false;
|
|
1134
|
+
|
|
1135
|
+
rl.on('close', () => {
|
|
1136
|
+
if (!answered) {
|
|
1137
|
+
answered = true;
|
|
1138
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
1139
|
+
process.exit(0);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}
|
|
1144
|
+
|
|
1145
|
+
${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
1146
|
+
${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
|
|
1147
|
+
${cyan}3${reset}) Both
|
|
1148
|
+
`);
|
|
1149
|
+
|
|
1150
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
1151
|
+
answered = true;
|
|
1152
|
+
rl.close();
|
|
1153
|
+
const choice = answer.trim() || '1';
|
|
1154
|
+
if (choice === '3') {
|
|
1155
|
+
callback(['claude', 'opencode']);
|
|
1156
|
+
} else if (choice === '2') {
|
|
1157
|
+
callback(['opencode']);
|
|
1158
|
+
} else {
|
|
1159
|
+
callback(['claude']);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
503
1164
|
/**
|
|
504
1165
|
* Prompt for install location
|
|
1166
|
+
* @param {string[]} runtimes - Array of runtimes to install for
|
|
505
1167
|
*/
|
|
506
|
-
function promptLocation() {
|
|
1168
|
+
function promptLocation(runtimes) {
|
|
507
1169
|
// Check if stdin is a TTY - if not, fall back to global install
|
|
508
1170
|
// This handles npx execution in environments like WSL2 where stdin may not be properly connected
|
|
509
1171
|
if (!process.stdin.isTTY) {
|
|
510
1172
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
|
|
511
|
-
|
|
512
|
-
handleStatusline(settings, false, (shouldInstallStatusline) => {
|
|
513
|
-
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
|
|
514
|
-
});
|
|
1173
|
+
installAllRuntimes(runtimes, true, false);
|
|
515
1174
|
return;
|
|
516
1175
|
}
|
|
517
1176
|
|
|
@@ -523,26 +1182,28 @@ function promptLocation() {
|
|
|
523
1182
|
// Track whether we've processed the answer to prevent double-execution
|
|
524
1183
|
let answered = false;
|
|
525
1184
|
|
|
526
|
-
// Handle readline close event
|
|
1185
|
+
// Handle readline close event (Ctrl+C, Escape, etc.) - cancel installation
|
|
527
1186
|
rl.on('close', () => {
|
|
528
1187
|
if (!answered) {
|
|
529
1188
|
answered = true;
|
|
530
|
-
console.log(`\n ${yellow}
|
|
531
|
-
|
|
532
|
-
handleStatusline(settings, false, (shouldInstallStatusline) => {
|
|
533
|
-
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
|
|
534
|
-
});
|
|
1189
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
1190
|
+
process.exit(0);
|
|
535
1191
|
}
|
|
536
1192
|
});
|
|
537
1193
|
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
1194
|
+
// Show paths for selected runtimes
|
|
1195
|
+
const pathExamples = runtimes.map(r => {
|
|
1196
|
+
// Use the proper global directory function for each runtime
|
|
1197
|
+
const globalPath = getGlobalDir(r, explicitConfigDir);
|
|
1198
|
+
return globalPath.replace(os.homedir(), '~');
|
|
1199
|
+
}).join(', ');
|
|
1200
|
+
|
|
1201
|
+
const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
|
|
541
1202
|
|
|
542
1203
|
console.log(` ${yellow}Where would you like to install?${reset}
|
|
543
1204
|
|
|
544
|
-
${cyan}1${reset}) Global ${dim}(${
|
|
545
|
-
${cyan}2${reset}) Local ${dim}(
|
|
1205
|
+
${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
|
|
1206
|
+
${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
|
|
546
1207
|
`);
|
|
547
1208
|
|
|
548
1209
|
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
@@ -550,14 +1211,44 @@ function promptLocation() {
|
|
|
550
1211
|
rl.close();
|
|
551
1212
|
const choice = answer.trim() || '1';
|
|
552
1213
|
const isGlobal = choice !== '2';
|
|
553
|
-
|
|
554
|
-
// Interactive mode - prompt for optional features
|
|
555
|
-
handleStatusline(settings, true, (shouldInstallStatusline) => {
|
|
556
|
-
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
|
|
557
|
-
});
|
|
1214
|
+
installAllRuntimes(runtimes, isGlobal, true);
|
|
558
1215
|
});
|
|
559
1216
|
}
|
|
560
1217
|
|
|
1218
|
+
/**
|
|
1219
|
+
* Install GSD for all selected runtimes
|
|
1220
|
+
* @param {string[]} runtimes - Array of runtimes to install for
|
|
1221
|
+
* @param {boolean} isGlobal - Whether to install globally
|
|
1222
|
+
* @param {boolean} isInteractive - Whether running interactively
|
|
1223
|
+
*/
|
|
1224
|
+
function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
1225
|
+
const results = [];
|
|
1226
|
+
|
|
1227
|
+
for (const runtime of runtimes) {
|
|
1228
|
+
const result = install(isGlobal, runtime);
|
|
1229
|
+
results.push(result);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Handle statusline for Claude Code only (OpenCode uses themes)
|
|
1233
|
+
const claudeResult = results.find(r => r.runtime === 'claude');
|
|
1234
|
+
|
|
1235
|
+
if (claudeResult) {
|
|
1236
|
+
handleStatusline(claudeResult.settings, isInteractive, (shouldInstallStatusline) => {
|
|
1237
|
+
finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude');
|
|
1238
|
+
|
|
1239
|
+
// Finish OpenCode install if present
|
|
1240
|
+
const opencodeResult = results.find(r => r.runtime === 'opencode');
|
|
1241
|
+
if (opencodeResult) {
|
|
1242
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
} else {
|
|
1246
|
+
// Only OpenCode
|
|
1247
|
+
const opencodeResult = results[0];
|
|
1248
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
561
1252
|
// Main
|
|
562
1253
|
if (hasGlobal && hasLocal) {
|
|
563
1254
|
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
@@ -565,18 +1256,37 @@ if (hasGlobal && hasLocal) {
|
|
|
565
1256
|
} else if (explicitConfigDir && hasLocal) {
|
|
566
1257
|
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
567
1258
|
process.exit(1);
|
|
568
|
-
} else if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1259
|
+
} else if (hasUninstall) {
|
|
1260
|
+
// Uninstall mode
|
|
1261
|
+
if (!hasGlobal && !hasLocal) {
|
|
1262
|
+
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
1263
|
+
console.error(` Example: npx get-shit-done-cc --claude --global --uninstall`);
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
|
1267
|
+
for (const runtime of runtimes) {
|
|
1268
|
+
uninstall(hasGlobal, runtime);
|
|
1269
|
+
}
|
|
1270
|
+
} else if (selectedRuntimes.length > 0) {
|
|
1271
|
+
// Non-interactive: runtime specified via flags
|
|
1272
|
+
if (!hasGlobal && !hasLocal) {
|
|
1273
|
+
// Need location but runtime is specified - prompt for location only
|
|
1274
|
+
promptLocation(selectedRuntimes);
|
|
1275
|
+
} else {
|
|
1276
|
+
// Both runtime and location specified via flags
|
|
1277
|
+
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
|
1278
|
+
}
|
|
1279
|
+
} else if (hasGlobal || hasLocal) {
|
|
1280
|
+
// Location specified but no runtime - default to Claude Code
|
|
1281
|
+
installAllRuntimes(['claude'], hasGlobal, false);
|
|
580
1282
|
} else {
|
|
581
|
-
|
|
1283
|
+
// Fully interactive: prompt for runtime, then location
|
|
1284
|
+
if (!process.stdin.isTTY) {
|
|
1285
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
1286
|
+
installAllRuntimes(['claude'], true, false);
|
|
1287
|
+
} else {
|
|
1288
|
+
promptRuntime((runtimes) => {
|
|
1289
|
+
promptLocation(runtimes);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
582
1292
|
}
|