get-shit-done-cc 1.9.3 → 1.9.6
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 +26 -5
- package/agents/gsd-executor.md +15 -0
- package/bin/install.js +467 -107
- package/commands/gsd/new-project.md +15 -11
- package/commands/gsd/plan-phase.md +6 -6
- package/commands/gsd/research-phase.md +4 -4
- package/get-shit-done/references/checkpoints.md +318 -28
- package/get-shit-done/references/verification-patterns.md +17 -0
- package/get-shit-done/templates/context.md +0 -8
- package/get-shit-done/templates/phase-prompt.md +18 -27
- 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/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 +2 -10
- package/get-shit-done/workflows/transition.md +1 -9
- package/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -15,6 +15,29 @@ 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
|
+
|
|
26
|
+
// Runtime selection - can be set by flags or interactive prompt
|
|
27
|
+
let selectedRuntimes = [];
|
|
28
|
+
if (hasBoth) {
|
|
29
|
+
selectedRuntimes = ['claude', 'opencode'];
|
|
30
|
+
} else if (hasOpencode) {
|
|
31
|
+
selectedRuntimes = ['opencode'];
|
|
32
|
+
} else if (hasClaude) {
|
|
33
|
+
selectedRuntimes = ['claude'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to get directory name for a runtime
|
|
37
|
+
function getDirName(runtime) {
|
|
38
|
+
return runtime === 'opencode' ? '.opencode' : '.claude';
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
const banner = `
|
|
19
42
|
${cyan} ██████╗ ███████╗██████╗
|
|
20
43
|
██╔════╝ ██╔════╝██╔══██╗
|
|
@@ -25,14 +48,9 @@ ${cyan} ██████╗ ███████╗██████╗
|
|
|
25
48
|
|
|
26
49
|
Get Shit Done ${dim}v${pkg.version}${reset}
|
|
27
50
|
A meta-prompting, context engineering and spec-driven
|
|
28
|
-
development system for Claude Code by TÂCHES.
|
|
51
|
+
development system for Claude Code (and opencode) by TÂCHES.
|
|
29
52
|
`;
|
|
30
53
|
|
|
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
54
|
// Parse --config-dir argument
|
|
37
55
|
function parseConfigDirArg() {
|
|
38
56
|
const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
|
|
@@ -68,24 +86,33 @@ if (hasHelp) {
|
|
|
68
86
|
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]
|
|
69
87
|
|
|
70
88
|
${yellow}Options:${reset}
|
|
71
|
-
${cyan}-g, --global${reset} Install globally (to
|
|
72
|
-
${cyan}-l, --local${reset} Install locally (to
|
|
73
|
-
${cyan}
|
|
89
|
+
${cyan}-g, --global${reset} Install globally (to config directory)
|
|
90
|
+
${cyan}-l, --local${reset} Install locally (to current directory)
|
|
91
|
+
${cyan}--claude${reset} Install for Claude Code only
|
|
92
|
+
${cyan}--opencode${reset} Install for OpenCode only
|
|
93
|
+
${cyan}--both${reset} Install for both Claude Code and OpenCode
|
|
94
|
+
${cyan}-c, --config-dir <path>${reset} Specify custom config directory
|
|
74
95
|
${cyan}-h, --help${reset} Show this help message
|
|
75
96
|
${cyan}--force-statusline${reset} Replace existing statusline config
|
|
76
97
|
|
|
77
98
|
${yellow}Examples:${reset}
|
|
78
|
-
${dim}#
|
|
79
|
-
npx get-shit-done-cc
|
|
99
|
+
${dim}# Interactive install (prompts for runtime and location)${reset}
|
|
100
|
+
npx get-shit-done-cc
|
|
101
|
+
|
|
102
|
+
${dim}# Install for Claude Code globally${reset}
|
|
103
|
+
npx get-shit-done-cc --claude --global
|
|
104
|
+
|
|
105
|
+
${dim}# Install for OpenCode globally${reset}
|
|
106
|
+
npx get-shit-done-cc --opencode --global
|
|
80
107
|
|
|
81
|
-
${dim}# Install
|
|
82
|
-
npx get-shit-done-cc --
|
|
108
|
+
${dim}# Install for both runtimes globally${reset}
|
|
109
|
+
npx get-shit-done-cc --both --global
|
|
83
110
|
|
|
84
|
-
${dim}#
|
|
85
|
-
|
|
111
|
+
${dim}# Install to custom config directory${reset}
|
|
112
|
+
npx get-shit-done-cc --claude --global --config-dir ~/.claude-bc
|
|
86
113
|
|
|
87
114
|
${dim}# Install to current project only${reset}
|
|
88
|
-
npx get-shit-done-cc --local
|
|
115
|
+
npx get-shit-done-cc --claude --local
|
|
89
116
|
|
|
90
117
|
${yellow}Notes:${reset}
|
|
91
118
|
The --config-dir option is useful when you have multiple Claude Code
|
|
@@ -106,7 +133,17 @@ function expandTilde(filePath) {
|
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
/**
|
|
109
|
-
*
|
|
136
|
+
* Build a hook command path using forward slashes for cross-platform compatibility.
|
|
137
|
+
* On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
|
|
138
|
+
*/
|
|
139
|
+
function buildHookCommand(claudeDir, hookName) {
|
|
140
|
+
// Use forward slashes for Node.js compatibility on all platforms
|
|
141
|
+
const hooksPath = claudeDir.replace(/\\/g, '/') + '/hooks/' + hookName;
|
|
142
|
+
return `node "${hooksPath}"`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Read and parse settings.json, returning empty object if it doesn't exist
|
|
110
147
|
*/
|
|
111
148
|
function readSettings(settingsPath) {
|
|
112
149
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -126,11 +163,169 @@ function writeSettings(settingsPath, settings) {
|
|
|
126
163
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
127
164
|
}
|
|
128
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Convert Claude Code frontmatter to opencode format
|
|
168
|
+
* - Converts 'allowed-tools:' array to 'permission:' object
|
|
169
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
170
|
+
* @returns {string} - Content with converted frontmatter
|
|
171
|
+
*/
|
|
172
|
+
// Color name to hex mapping for opencode compatibility
|
|
173
|
+
const colorNameToHex = {
|
|
174
|
+
cyan: '#00FFFF',
|
|
175
|
+
red: '#FF0000',
|
|
176
|
+
green: '#00FF00',
|
|
177
|
+
blue: '#0000FF',
|
|
178
|
+
yellow: '#FFFF00',
|
|
179
|
+
magenta: '#FF00FF',
|
|
180
|
+
orange: '#FFA500',
|
|
181
|
+
purple: '#800080',
|
|
182
|
+
pink: '#FFC0CB',
|
|
183
|
+
white: '#FFFFFF',
|
|
184
|
+
black: '#000000',
|
|
185
|
+
gray: '#808080',
|
|
186
|
+
grey: '#808080',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Tool name mapping from Claude Code to OpenCode
|
|
190
|
+
// OpenCode uses lowercase tool names; special mappings for renamed tools
|
|
191
|
+
const claudeToOpencodeTools = {
|
|
192
|
+
AskUserQuestion: 'question',
|
|
193
|
+
SlashCommand: 'skill',
|
|
194
|
+
TodoWrite: 'todowrite',
|
|
195
|
+
WebFetch: 'webfetch',
|
|
196
|
+
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert a Claude Code tool name to OpenCode format
|
|
201
|
+
* - Applies special mappings (AskUserQuestion -> question, etc.)
|
|
202
|
+
* - Converts to lowercase (except MCP tools which keep their format)
|
|
203
|
+
*/
|
|
204
|
+
function convertToolName(claudeTool) {
|
|
205
|
+
// Check for special mapping first
|
|
206
|
+
if (claudeToOpencodeTools[claudeTool]) {
|
|
207
|
+
return claudeToOpencodeTools[claudeTool];
|
|
208
|
+
}
|
|
209
|
+
// MCP tools (mcp__*) keep their format
|
|
210
|
+
if (claudeTool.startsWith('mcp__')) {
|
|
211
|
+
return claudeTool;
|
|
212
|
+
}
|
|
213
|
+
// Default: convert to lowercase
|
|
214
|
+
return claudeTool.toLowerCase();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function convertClaudeToOpencodeFrontmatter(content) {
|
|
218
|
+
// Replace tool name references in content (applies to all files)
|
|
219
|
+
let convertedContent = content;
|
|
220
|
+
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
221
|
+
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
222
|
+
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
223
|
+
// Replace /gsd:command with /gsd/command for opencode
|
|
224
|
+
convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd/');
|
|
225
|
+
// Replace ~/.claude with ~/.opencode
|
|
226
|
+
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.opencode');
|
|
227
|
+
|
|
228
|
+
// Check if content has frontmatter
|
|
229
|
+
if (!convertedContent.startsWith('---')) {
|
|
230
|
+
return convertedContent;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Find the end of frontmatter
|
|
234
|
+
const endIndex = convertedContent.indexOf('---', 3);
|
|
235
|
+
if (endIndex === -1) {
|
|
236
|
+
return convertedContent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const frontmatter = convertedContent.substring(3, endIndex).trim();
|
|
240
|
+
const body = convertedContent.substring(endIndex + 3);
|
|
241
|
+
|
|
242
|
+
// Parse frontmatter line by line (simple YAML parsing)
|
|
243
|
+
const lines = frontmatter.split('\n');
|
|
244
|
+
const newLines = [];
|
|
245
|
+
let inAllowedTools = false;
|
|
246
|
+
const allowedTools = [];
|
|
247
|
+
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
const trimmed = line.trim();
|
|
250
|
+
|
|
251
|
+
// Detect start of allowed-tools array
|
|
252
|
+
if (trimmed.startsWith('allowed-tools:')) {
|
|
253
|
+
inAllowedTools = true;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Detect inline tools: field (comma-separated string)
|
|
258
|
+
if (trimmed.startsWith('tools:')) {
|
|
259
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
260
|
+
if (toolsValue) {
|
|
261
|
+
// Parse comma-separated tools
|
|
262
|
+
const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
|
|
263
|
+
allowedTools.push(...tools);
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Remove name: field - opencode uses filename for command name
|
|
269
|
+
if (trimmed.startsWith('name:')) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Convert color names to hex for opencode
|
|
274
|
+
if (trimmed.startsWith('color:')) {
|
|
275
|
+
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
276
|
+
const hexColor = colorNameToHex[colorValue];
|
|
277
|
+
if (hexColor) {
|
|
278
|
+
newLines.push(`color: "${hexColor}"`);
|
|
279
|
+
} else if (colorValue.startsWith('#')) {
|
|
280
|
+
// Already hex, keep as is
|
|
281
|
+
newLines.push(line);
|
|
282
|
+
}
|
|
283
|
+
// Skip unknown color names
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Collect allowed-tools items
|
|
288
|
+
if (inAllowedTools) {
|
|
289
|
+
if (trimmed.startsWith('- ')) {
|
|
290
|
+
allowedTools.push(trimmed.substring(2).trim());
|
|
291
|
+
continue;
|
|
292
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
293
|
+
// End of array, new field started
|
|
294
|
+
inAllowedTools = false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Keep other fields (including name: which opencode ignores)
|
|
299
|
+
if (!inAllowedTools) {
|
|
300
|
+
newLines.push(line);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Add tools object if we had allowed-tools or tools
|
|
305
|
+
if (allowedTools.length > 0) {
|
|
306
|
+
newLines.push('tools:');
|
|
307
|
+
for (const tool of allowedTools) {
|
|
308
|
+
newLines.push(` ${convertToolName(tool)}: true`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Rebuild frontmatter (body already has tool names converted)
|
|
313
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
314
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
129
317
|
/**
|
|
130
318
|
* Recursively copy directory, replacing paths in .md files
|
|
131
319
|
* Deletes existing destDir first to remove orphaned files from previous versions
|
|
320
|
+
* @param {string} srcDir - Source directory
|
|
321
|
+
* @param {string} destDir - Destination directory
|
|
322
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
323
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
132
324
|
*/
|
|
133
|
-
function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
|
|
325
|
+
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
|
|
326
|
+
const isOpencode = runtime === 'opencode';
|
|
327
|
+
const dirName = getDirName(runtime);
|
|
328
|
+
|
|
134
329
|
// Clean install: remove existing destination to prevent orphaned files
|
|
135
330
|
if (fs.existsSync(destDir)) {
|
|
136
331
|
fs.rmSync(destDir, { recursive: true });
|
|
@@ -144,11 +339,16 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
|
|
|
144
339
|
const destPath = path.join(destDir, entry.name);
|
|
145
340
|
|
|
146
341
|
if (entry.isDirectory()) {
|
|
147
|
-
copyWithPathReplacement(srcPath, destPath, pathPrefix);
|
|
342
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime);
|
|
148
343
|
} else if (entry.name.endsWith('.md')) {
|
|
149
|
-
// Replace ~/.claude/ with the appropriate prefix in
|
|
344
|
+
// Replace ~/.claude/ with the appropriate prefix in Markdown files
|
|
150
345
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
151
|
-
|
|
346
|
+
const claudeDirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
|
|
347
|
+
content = content.replace(claudeDirRegex, pathPrefix);
|
|
348
|
+
// Convert frontmatter for opencode compatibility
|
|
349
|
+
if (isOpencode) {
|
|
350
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
351
|
+
}
|
|
152
352
|
fs.writeFileSync(destPath, content);
|
|
153
353
|
} else {
|
|
154
354
|
fs.copyFileSync(srcPath, destPath);
|
|
@@ -219,6 +419,59 @@ function cleanupOrphanedHooks(settings) {
|
|
|
219
419
|
return settings;
|
|
220
420
|
}
|
|
221
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Configure OpenCode permissions to allow reading GSD reference docs
|
|
424
|
+
* This prevents permission prompts when GSD accesses ~/.opencode/get-shit-done/
|
|
425
|
+
*/
|
|
426
|
+
function configureOpencodePermissions() {
|
|
427
|
+
const configPath = path.join(os.homedir(), '.opencode.json');
|
|
428
|
+
|
|
429
|
+
// Read existing config or create empty object
|
|
430
|
+
let config = {};
|
|
431
|
+
if (fs.existsSync(configPath)) {
|
|
432
|
+
try {
|
|
433
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
434
|
+
} catch (e) {
|
|
435
|
+
// Invalid JSON - start fresh but warn user
|
|
436
|
+
console.log(` ${yellow}⚠${reset} ~/.opencode.json had invalid JSON, recreating`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Ensure permission structure exists
|
|
441
|
+
if (!config.permission) {
|
|
442
|
+
config.permission = {};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const gsdPath = '~/.opencode/get-shit-done/*';
|
|
446
|
+
let modified = false;
|
|
447
|
+
|
|
448
|
+
// Configure read permission
|
|
449
|
+
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
450
|
+
config.permission.read = {};
|
|
451
|
+
}
|
|
452
|
+
if (config.permission.read[gsdPath] !== 'allow') {
|
|
453
|
+
config.permission.read[gsdPath] = 'allow';
|
|
454
|
+
modified = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Configure external_directory permission (the safety guard for paths outside project)
|
|
458
|
+
if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
|
|
459
|
+
config.permission.external_directory = {};
|
|
460
|
+
}
|
|
461
|
+
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
|
462
|
+
config.permission.external_directory[gsdPath] = 'allow';
|
|
463
|
+
modified = true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!modified) {
|
|
467
|
+
return; // Already configured
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Write config back
|
|
471
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
472
|
+
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
473
|
+
}
|
|
474
|
+
|
|
222
475
|
/**
|
|
223
476
|
* Verify a directory exists and contains files
|
|
224
477
|
*/
|
|
@@ -252,43 +505,49 @@ function verifyFileInstalled(filePath, description) {
|
|
|
252
505
|
}
|
|
253
506
|
|
|
254
507
|
/**
|
|
255
|
-
* Install to the specified directory
|
|
508
|
+
* Install to the specified directory for a specific runtime
|
|
509
|
+
* @param {boolean} isGlobal - Whether to install globally or locally
|
|
510
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
256
511
|
*/
|
|
257
|
-
function install(isGlobal) {
|
|
512
|
+
function install(isGlobal, runtime = 'claude') {
|
|
513
|
+
const isOpencode = runtime === 'opencode';
|
|
514
|
+
const dirName = getDirName(runtime);
|
|
258
515
|
const src = path.join(__dirname, '..');
|
|
259
|
-
|
|
516
|
+
|
|
517
|
+
// Priority: explicit --config-dir arg > CLAUDE_CONFIG_DIR env var > default dir
|
|
260
518
|
const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
261
|
-
const defaultGlobalDir = configDir || path.join(os.homedir(),
|
|
262
|
-
const
|
|
519
|
+
const defaultGlobalDir = configDir || path.join(os.homedir(), dirName);
|
|
520
|
+
const targetDir = isGlobal
|
|
263
521
|
? defaultGlobalDir
|
|
264
|
-
: path.join(process.cwd(),
|
|
522
|
+
: path.join(process.cwd(), dirName);
|
|
265
523
|
|
|
266
524
|
const locationLabel = isGlobal
|
|
267
|
-
?
|
|
268
|
-
:
|
|
525
|
+
? targetDir.replace(os.homedir(), '~')
|
|
526
|
+
: targetDir.replace(process.cwd(), '.');
|
|
269
527
|
|
|
270
528
|
// Path prefix for file references
|
|
271
529
|
// Use actual path when CLAUDE_CONFIG_DIR is set, otherwise use ~ shorthand
|
|
272
530
|
const pathPrefix = isGlobal
|
|
273
|
-
? (configDir ? `${
|
|
274
|
-
:
|
|
531
|
+
? (configDir ? `${targetDir}/` : `~/${dirName}/`)
|
|
532
|
+
: `./${dirName}/`;
|
|
275
533
|
|
|
276
|
-
|
|
534
|
+
const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
535
|
+
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
277
536
|
|
|
278
537
|
// Track installation failures
|
|
279
538
|
const failures = [];
|
|
280
539
|
|
|
281
540
|
// Clean up orphaned files from previous versions
|
|
282
|
-
cleanupOrphanedFiles(
|
|
541
|
+
cleanupOrphanedFiles(targetDir);
|
|
283
542
|
|
|
284
543
|
// Create commands directory
|
|
285
|
-
const commandsDir = path.join(
|
|
544
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
286
545
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
287
546
|
|
|
288
547
|
// Copy commands/gsd with path replacement
|
|
289
548
|
const gsdSrc = path.join(src, 'commands', 'gsd');
|
|
290
549
|
const gsdDest = path.join(commandsDir, 'gsd');
|
|
291
|
-
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix);
|
|
550
|
+
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime);
|
|
292
551
|
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
|
293
552
|
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
|
294
553
|
} else {
|
|
@@ -297,19 +556,19 @@ function install(isGlobal) {
|
|
|
297
556
|
|
|
298
557
|
// Copy get-shit-done skill with path replacement
|
|
299
558
|
const skillSrc = path.join(src, 'get-shit-done');
|
|
300
|
-
const skillDest = path.join(
|
|
301
|
-
copyWithPathReplacement(skillSrc, skillDest, pathPrefix);
|
|
559
|
+
const skillDest = path.join(targetDir, 'get-shit-done');
|
|
560
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime);
|
|
302
561
|
if (verifyInstalled(skillDest, 'get-shit-done')) {
|
|
303
562
|
console.log(` ${green}✓${reset} Installed get-shit-done`);
|
|
304
563
|
} else {
|
|
305
564
|
failures.push('get-shit-done');
|
|
306
565
|
}
|
|
307
566
|
|
|
308
|
-
// Copy agents to
|
|
567
|
+
// Copy agents to agents directory (subagents must be at root level)
|
|
309
568
|
// Only delete gsd-*.md files to preserve user's custom agents
|
|
310
569
|
const agentsSrc = path.join(src, 'agents');
|
|
311
570
|
if (fs.existsSync(agentsSrc)) {
|
|
312
|
-
const agentsDest = path.join(
|
|
571
|
+
const agentsDest = path.join(targetDir, 'agents');
|
|
313
572
|
fs.mkdirSync(agentsDest, { recursive: true });
|
|
314
573
|
|
|
315
574
|
// Remove old GSD agents (gsd-*.md) before copying new ones
|
|
@@ -326,7 +585,12 @@ function install(isGlobal) {
|
|
|
326
585
|
for (const entry of agentEntries) {
|
|
327
586
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
328
587
|
let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
|
|
329
|
-
|
|
588
|
+
const dirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
|
|
589
|
+
content = content.replace(dirRegex, pathPrefix);
|
|
590
|
+
// Convert frontmatter for opencode compatibility
|
|
591
|
+
if (isOpencode) {
|
|
592
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
593
|
+
}
|
|
330
594
|
fs.writeFileSync(path.join(agentsDest, entry.name), content);
|
|
331
595
|
}
|
|
332
596
|
}
|
|
@@ -339,7 +603,7 @@ function install(isGlobal) {
|
|
|
339
603
|
|
|
340
604
|
// Copy CHANGELOG.md
|
|
341
605
|
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
342
|
-
const changelogDest = path.join(
|
|
606
|
+
const changelogDest = path.join(targetDir, 'get-shit-done', 'CHANGELOG.md');
|
|
343
607
|
if (fs.existsSync(changelogSrc)) {
|
|
344
608
|
fs.copyFileSync(changelogSrc, changelogDest);
|
|
345
609
|
if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
|
|
@@ -350,7 +614,7 @@ function install(isGlobal) {
|
|
|
350
614
|
}
|
|
351
615
|
|
|
352
616
|
// Write VERSION file for whats-new command
|
|
353
|
-
const versionDest = path.join(
|
|
617
|
+
const versionDest = path.join(targetDir, 'get-shit-done', 'VERSION');
|
|
354
618
|
fs.writeFileSync(versionDest, pkg.version);
|
|
355
619
|
if (verifyFileInstalled(versionDest, 'VERSION')) {
|
|
356
620
|
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
@@ -361,7 +625,7 @@ function install(isGlobal) {
|
|
|
361
625
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
362
626
|
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
363
627
|
if (fs.existsSync(hooksSrc)) {
|
|
364
|
-
const hooksDest = path.join(
|
|
628
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
365
629
|
fs.mkdirSync(hooksDest, { recursive: true });
|
|
366
630
|
const hookEntries = fs.readdirSync(hooksSrc);
|
|
367
631
|
for (const entry of hookEntries) {
|
|
@@ -387,48 +651,57 @@ function install(isGlobal) {
|
|
|
387
651
|
}
|
|
388
652
|
|
|
389
653
|
// Configure statusline and hooks in settings.json
|
|
390
|
-
const settingsPath = path.join(
|
|
654
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
391
655
|
const settings = cleanupOrphanedHooks(readSettings(settingsPath));
|
|
392
656
|
const statuslineCommand = isGlobal
|
|
393
|
-
? '
|
|
394
|
-
: 'node
|
|
657
|
+
? buildHookCommand(targetDir, 'gsd-statusline.js')
|
|
658
|
+
: 'node ' + dirName + '/hooks/gsd-statusline.js';
|
|
395
659
|
const updateCheckCommand = isGlobal
|
|
396
|
-
? '
|
|
397
|
-
: 'node
|
|
660
|
+
? buildHookCommand(targetDir, 'gsd-check-update.js')
|
|
661
|
+
: 'node ' + dirName + '/hooks/gsd-check-update.js';
|
|
398
662
|
|
|
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
|
-
);
|
|
663
|
+
// Configure SessionStart hook for update checking (skip for opencode - different hook system)
|
|
664
|
+
if (!isOpencode) {
|
|
665
|
+
if (!settings.hooks) {
|
|
666
|
+
settings.hooks = {};
|
|
667
|
+
}
|
|
668
|
+
if (!settings.hooks.SessionStart) {
|
|
669
|
+
settings.hooks.SessionStart = [];
|
|
670
|
+
}
|
|
411
671
|
|
|
412
|
-
|
|
413
|
-
settings.hooks.SessionStart.
|
|
414
|
-
hooks
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
672
|
+
// Check if GSD update hook already exists
|
|
673
|
+
const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
|
|
674
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update'))
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
if (!hasGsdUpdateHook) {
|
|
678
|
+
settings.hooks.SessionStart.push({
|
|
679
|
+
hooks: [
|
|
680
|
+
{
|
|
681
|
+
type: 'command',
|
|
682
|
+
command: updateCheckCommand
|
|
683
|
+
}
|
|
684
|
+
]
|
|
685
|
+
});
|
|
686
|
+
console.log(` ${green}✓${reset} Configured update check hook`);
|
|
687
|
+
}
|
|
422
688
|
}
|
|
423
689
|
|
|
424
|
-
return { settingsPath, settings, statuslineCommand };
|
|
690
|
+
return { settingsPath, settings, statuslineCommand, runtime };
|
|
425
691
|
}
|
|
426
692
|
|
|
427
693
|
/**
|
|
428
694
|
* Apply statusline config, then print completion message
|
|
695
|
+
* @param {string} settingsPath - Path to settings.json
|
|
696
|
+
* @param {object} settings - Settings object
|
|
697
|
+
* @param {string} statuslineCommand - Statusline command
|
|
698
|
+
* @param {boolean} shouldInstallStatusline - Whether to install statusline
|
|
699
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
429
700
|
*/
|
|
430
|
-
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline) {
|
|
431
|
-
|
|
701
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude') {
|
|
702
|
+
const isOpencode = runtime === 'opencode';
|
|
703
|
+
|
|
704
|
+
if (shouldInstallStatusline && !isOpencode) {
|
|
432
705
|
settings.statusLine = {
|
|
433
706
|
type: 'command',
|
|
434
707
|
command: statuslineCommand
|
|
@@ -439,8 +712,15 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
439
712
|
// Always write settings (hooks were already configured in install())
|
|
440
713
|
writeSettings(settingsPath, settings);
|
|
441
714
|
|
|
715
|
+
// Configure OpenCode permissions if needed
|
|
716
|
+
if (isOpencode) {
|
|
717
|
+
configureOpencodePermissions();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const program = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
721
|
+
const command = isOpencode ? '/gsd/help' : '/gsd:help';
|
|
442
722
|
console.log(`
|
|
443
|
-
${green}Done!${reset} Launch
|
|
723
|
+
${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
|
|
444
724
|
`);
|
|
445
725
|
}
|
|
446
726
|
|
|
@@ -500,18 +780,57 @@ function handleStatusline(settings, isInteractive, callback) {
|
|
|
500
780
|
});
|
|
501
781
|
}
|
|
502
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Prompt for runtime selection (Claude Code / OpenCode / Both)
|
|
785
|
+
* @param {function} callback - Called with array of selected runtimes
|
|
786
|
+
*/
|
|
787
|
+
function promptRuntime(callback) {
|
|
788
|
+
const rl = readline.createInterface({
|
|
789
|
+
input: process.stdin,
|
|
790
|
+
output: process.stdout
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
let answered = false;
|
|
794
|
+
|
|
795
|
+
rl.on('close', () => {
|
|
796
|
+
if (!answered) {
|
|
797
|
+
answered = true;
|
|
798
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
799
|
+
process.exit(0);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}
|
|
804
|
+
|
|
805
|
+
${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
806
|
+
${cyan}2${reset}) OpenCode ${dim}(~/.opencode)${reset} - open source, free models
|
|
807
|
+
${cyan}3${reset}) Both
|
|
808
|
+
`);
|
|
809
|
+
|
|
810
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
811
|
+
answered = true;
|
|
812
|
+
rl.close();
|
|
813
|
+
const choice = answer.trim() || '1';
|
|
814
|
+
if (choice === '3') {
|
|
815
|
+
callback(['claude', 'opencode']);
|
|
816
|
+
} else if (choice === '2') {
|
|
817
|
+
callback(['opencode']);
|
|
818
|
+
} else {
|
|
819
|
+
callback(['claude']);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
503
824
|
/**
|
|
504
825
|
* Prompt for install location
|
|
826
|
+
* @param {string[]} runtimes - Array of runtimes to install for
|
|
505
827
|
*/
|
|
506
|
-
function promptLocation() {
|
|
828
|
+
function promptLocation(runtimes) {
|
|
507
829
|
// Check if stdin is a TTY - if not, fall back to global install
|
|
508
830
|
// This handles npx execution in environments like WSL2 where stdin may not be properly connected
|
|
509
831
|
if (!process.stdin.isTTY) {
|
|
510
832
|
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
|
-
});
|
|
833
|
+
installAllRuntimes(runtimes, true, false);
|
|
515
834
|
return;
|
|
516
835
|
}
|
|
517
836
|
|
|
@@ -523,26 +842,29 @@ function promptLocation() {
|
|
|
523
842
|
// Track whether we've processed the answer to prevent double-execution
|
|
524
843
|
let answered = false;
|
|
525
844
|
|
|
526
|
-
// Handle readline close event
|
|
845
|
+
// Handle readline close event (Ctrl+C, Escape, etc.) - cancel installation
|
|
527
846
|
rl.on('close', () => {
|
|
528
847
|
if (!answered) {
|
|
529
848
|
answered = true;
|
|
530
|
-
console.log(`\n ${yellow}
|
|
531
|
-
|
|
532
|
-
handleStatusline(settings, false, (shouldInstallStatusline) => {
|
|
533
|
-
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
|
|
534
|
-
});
|
|
849
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
850
|
+
process.exit(0);
|
|
535
851
|
}
|
|
536
852
|
});
|
|
537
853
|
|
|
854
|
+
// Show paths for selected runtimes
|
|
538
855
|
const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
539
|
-
const
|
|
540
|
-
|
|
856
|
+
const pathExamples = runtimes.map(r => {
|
|
857
|
+
const dir = getDirName(r);
|
|
858
|
+
const globalPath = configDir || path.join(os.homedir(), dir);
|
|
859
|
+
return globalPath.replace(os.homedir(), '~');
|
|
860
|
+
}).join(', ');
|
|
861
|
+
|
|
862
|
+
const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
|
|
541
863
|
|
|
542
864
|
console.log(` ${yellow}Where would you like to install?${reset}
|
|
543
865
|
|
|
544
|
-
${cyan}1${reset}) Global ${dim}(${
|
|
545
|
-
${cyan}2${reset}) Local ${dim}(
|
|
866
|
+
${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
|
|
867
|
+
${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
|
|
546
868
|
`);
|
|
547
869
|
|
|
548
870
|
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
@@ -550,14 +872,44 @@ function promptLocation() {
|
|
|
550
872
|
rl.close();
|
|
551
873
|
const choice = answer.trim() || '1';
|
|
552
874
|
const isGlobal = choice !== '2';
|
|
553
|
-
|
|
554
|
-
// Interactive mode - prompt for optional features
|
|
555
|
-
handleStatusline(settings, true, (shouldInstallStatusline) => {
|
|
556
|
-
finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
|
|
557
|
-
});
|
|
875
|
+
installAllRuntimes(runtimes, isGlobal, true);
|
|
558
876
|
});
|
|
559
877
|
}
|
|
560
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Install GSD for all selected runtimes
|
|
881
|
+
* @param {string[]} runtimes - Array of runtimes to install for
|
|
882
|
+
* @param {boolean} isGlobal - Whether to install globally
|
|
883
|
+
* @param {boolean} isInteractive - Whether running interactively
|
|
884
|
+
*/
|
|
885
|
+
function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
886
|
+
const results = [];
|
|
887
|
+
|
|
888
|
+
for (const runtime of runtimes) {
|
|
889
|
+
const result = install(isGlobal, runtime);
|
|
890
|
+
results.push(result);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Handle statusline for Claude Code only (OpenCode uses themes)
|
|
894
|
+
const claudeResult = results.find(r => r.runtime === 'claude');
|
|
895
|
+
|
|
896
|
+
if (claudeResult) {
|
|
897
|
+
handleStatusline(claudeResult.settings, isInteractive, (shouldInstallStatusline) => {
|
|
898
|
+
finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude');
|
|
899
|
+
|
|
900
|
+
// Finish OpenCode install if present
|
|
901
|
+
const opencodeResult = results.find(r => r.runtime === 'opencode');
|
|
902
|
+
if (opencodeResult) {
|
|
903
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
} else {
|
|
907
|
+
// Only OpenCode
|
|
908
|
+
const opencodeResult = results[0];
|
|
909
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
561
913
|
// Main
|
|
562
914
|
if (hasGlobal && hasLocal) {
|
|
563
915
|
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
@@ -565,18 +917,26 @@ if (hasGlobal && hasLocal) {
|
|
|
565
917
|
} else if (explicitConfigDir && hasLocal) {
|
|
566
918
|
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
567
919
|
process.exit(1);
|
|
568
|
-
} else if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
920
|
+
} else if (selectedRuntimes.length > 0) {
|
|
921
|
+
// Non-interactive: runtime specified via flags
|
|
922
|
+
if (!hasGlobal && !hasLocal) {
|
|
923
|
+
// Need location but runtime is specified - prompt for location only
|
|
924
|
+
promptLocation(selectedRuntimes);
|
|
925
|
+
} else {
|
|
926
|
+
// Both runtime and location specified via flags
|
|
927
|
+
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
|
928
|
+
}
|
|
929
|
+
} else if (hasGlobal || hasLocal) {
|
|
930
|
+
// Location specified but no runtime - default to Claude Code
|
|
931
|
+
installAllRuntimes(['claude'], hasGlobal, false);
|
|
580
932
|
} else {
|
|
581
|
-
|
|
933
|
+
// Fully interactive: prompt for runtime, then location
|
|
934
|
+
if (!process.stdin.isTTY) {
|
|
935
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
936
|
+
installAllRuntimes(['claude'], true, false);
|
|
937
|
+
} else {
|
|
938
|
+
promptRuntime((runtimes) => {
|
|
939
|
+
promptLocation(runtimes);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
582
942
|
}
|