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/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 Claude config directory)
72
- ${cyan}-l, --local${reset} Install locally (to ./.claude in current directory)
73
- ${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
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}# Install to default ~/.claude directory${reset}
79
- npx get-shit-done-cc --global
150
+ ${dim}# Interactive install (prompts for runtime and location)${reset}
151
+ npx get-shit-done-cc
80
152
 
81
- ${dim}# Install to custom config directory (for multiple Claude accounts)${reset}
82
- npx get-shit-done-cc --global --config-dir ~/.claude-bc
153
+ ${dim}# Install for Claude Code globally${reset}
154
+ npx get-shit-done-cc --claude --global
83
155
 
84
- ${dim}# Using environment variable${reset}
85
- CLAUDE_CONFIG_DIR=~/.claude-bc npx get-shit-done-cc --global
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
- * Read and parse settings.json, returning empty object if doesn't exist
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 markdown files
458
+ // Replace ~/.claude/ with the appropriate prefix in Markdown files
150
459
  let content = fs.readFileSync(srcPath, 'utf8');
151
- content = content.replace(/~\/\.claude\//g, pathPrefix);
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
- // Priority: explicit --config-dir arg > CLAUDE_CONFIG_DIR env var > default ~/.claude
260
- const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
261
- const defaultGlobalDir = configDir || path.join(os.homedir(), '.claude');
262
- const claudeDir = isGlobal
263
- ? defaultGlobalDir
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
- ? claudeDir.replace(os.homedir(), '~')
268
- : claudeDir.replace(process.cwd(), '.');
845
+ ? targetDir.replace(os.homedir(), '~')
846
+ : targetDir.replace(process.cwd(), '.');
269
847
 
270
- // Path prefix for file references
271
- // Use actual path when CLAUDE_CONFIG_DIR is set, otherwise use ~ shorthand
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
- ? (configDir ? `${claudeDir}/` : '~/.claude/')
274
- : './.claude/';
852
+ ? `${targetDir}/`
853
+ : `./${dirName}/`;
275
854
 
276
- console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
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(claudeDir);
283
-
284
- // Create commands directory
285
- const commandsDir = path.join(claudeDir, 'commands');
286
- fs.mkdirSync(commandsDir, { recursive: true });
287
-
288
- // Copy commands/gsd with path replacement
289
- const gsdSrc = path.join(src, 'commands', 'gsd');
290
- const gsdDest = path.join(commandsDir, 'gsd');
291
- copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix);
292
- if (verifyInstalled(gsdDest, 'commands/gsd')) {
293
- console.log(` ${green}✓${reset} Installed commands/gsd`);
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
- failures.push('commands/gsd');
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(claudeDir, 'get-shit-done');
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 ~/.claude/agents (subagents must be at root level)
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(claudeDir, 'agents');
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
- content = content.replace(/~\/\.claude\//g, pathPrefix);
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(claudeDir, 'get-shit-done', 'CHANGELOG.md');
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(claudeDir, 'get-shit-done', 'VERSION');
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(claudeDir, 'hooks');
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(claudeDir, 'settings.json');
992
+ const settingsPath = path.join(targetDir, 'settings.json');
391
993
  const settings = cleanupOrphanedHooks(readSettings(settingsPath));
392
994
  const statuslineCommand = isGlobal
393
- ? 'node "$HOME/.claude/hooks/gsd-statusline.js"'
394
- : 'node .claude/hooks/gsd-statusline.js';
995
+ ? buildHookCommand(targetDir, 'gsd-statusline.js')
996
+ : 'node ' + dirName + '/hooks/gsd-statusline.js';
395
997
  const updateCheckCommand = isGlobal
396
- ? 'node "$HOME/.claude/hooks/gsd-check-update.js"'
397
- : 'node .claude/hooks/gsd-check-update.js';
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 (!settings.hooks) {
401
- settings.hooks = {};
402
- }
403
- if (!settings.hooks.SessionStart) {
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
- if (!hasGsdUpdateHook) {
413
- settings.hooks.SessionStart.push({
414
- hooks: [
415
- {
416
- type: 'command',
417
- command: updateCheckCommand
418
- }
419
- ]
420
- });
421
- console.log(` ${green}✓${reset} Configured update check hook`);
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
- if (shouldInstallStatusline) {
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 Claude Code and run ${cyan}/gsd:help${reset}.
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
- const { settingsPath, settings, statuslineCommand } = install(true);
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 to detect premature stdin closure
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}Input stream closed, defaulting to global install${reset}\n`);
531
- const { settingsPath, settings, statuslineCommand } = install(true);
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
- const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
539
- const globalPath = configDir || path.join(os.homedir(), '.claude');
540
- const globalLabel = globalPath.replace(os.homedir(), '~');
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}(${globalLabel})${reset} - available in all projects
545
- ${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
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
- const { settingsPath, settings, statuslineCommand } = install(isGlobal);
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 (hasGlobal) {
569
- const { settingsPath, settings, statuslineCommand } = install(true);
570
- // Non-interactive - respect flags
571
- handleStatusline(settings, false, (shouldInstallStatusline) => {
572
- finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
573
- });
574
- } else if (hasLocal) {
575
- const { settingsPath, settings, statuslineCommand } = install(false);
576
- // Non-interactive - respect flags
577
- handleStatusline(settings, false, (shouldInstallStatusline) => {
578
- finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
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
- promptLocation();
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
  }