get-shit-done-cc 1.10.0-experimental.0 → 1.10.0

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.
Files changed (61) hide show
  1. package/README.md +16 -17
  2. package/agents/gsd-executor.md +375 -37
  3. package/agents/gsd-planner.md +108 -15
  4. package/bin/install.js +163 -238
  5. package/commands/gsd/help.md +0 -43
  6. package/commands/gsd/new-project.md +8 -94
  7. package/commands/gsd/plan-phase.md +5 -35
  8. package/get-shit-done/references/verification-patterns.md +1 -1
  9. package/get-shit-done/templates/phase-prompt.md +4 -4
  10. package/get-shit-done/templates/state.md +0 -37
  11. package/get-shit-done/workflows/execute-phase.md +1 -44
  12. package/get-shit-done/workflows/execute-plan.md +856 -34
  13. package/hooks/dist/gsd-statusline.js +9 -6
  14. package/package.json +7 -10
  15. package/agents/design-specialist.md +0 -222
  16. package/commands/gsd/autopilot.md +0 -518
  17. package/commands/gsd/checkpoints.md +0 -229
  18. package/commands/gsd/design-system.md +0 -70
  19. package/commands/gsd/discuss-design.md +0 -77
  20. package/commands/gsd/extend.md +0 -80
  21. package/get-shit-done/references/ccr-integration.md +0 -468
  22. package/get-shit-done/references/checkpoint-execution.md +0 -369
  23. package/get-shit-done/references/checkpoint-types.md +0 -728
  24. package/get-shit-done/references/deviation-rules.md +0 -215
  25. package/get-shit-done/references/framework-patterns.md +0 -543
  26. package/get-shit-done/references/ui-principles.md +0 -258
  27. package/get-shit-done/skills/gsd-extend/SKILL.md +0 -154
  28. package/get-shit-done/skills/gsd-extend/references/agent-structure.md +0 -305
  29. package/get-shit-done/skills/gsd-extend/references/extension-anatomy.md +0 -123
  30. package/get-shit-done/skills/gsd-extend/references/reference-structure.md +0 -408
  31. package/get-shit-done/skills/gsd-extend/references/template-structure.md +0 -370
  32. package/get-shit-done/skills/gsd-extend/references/validation-rules.md +0 -140
  33. package/get-shit-done/skills/gsd-extend/references/workflow-structure.md +0 -253
  34. package/get-shit-done/skills/gsd-extend/templates/agent-template.md +0 -234
  35. package/get-shit-done/skills/gsd-extend/templates/reference-template.md +0 -239
  36. package/get-shit-done/skills/gsd-extend/templates/workflow-template.md +0 -169
  37. package/get-shit-done/skills/gsd-extend/workflows/create-approach.md +0 -332
  38. package/get-shit-done/skills/gsd-extend/workflows/list-extensions.md +0 -133
  39. package/get-shit-done/skills/gsd-extend/workflows/remove-extension.md +0 -93
  40. package/get-shit-done/skills/gsd-extend/workflows/validate-extension.md +0 -184
  41. package/get-shit-done/templates/autopilot-script-simple.sh +0 -181
  42. package/get-shit-done/templates/autopilot-script.sh +0 -1142
  43. package/get-shit-done/templates/autopilot-script.sh.backup +0 -1142
  44. package/get-shit-done/templates/design-system.md +0 -238
  45. package/get-shit-done/templates/phase-design.md +0 -205
  46. package/get-shit-done/templates/phase-models-template.json +0 -71
  47. package/get-shit-done/tui/App.tsx +0 -169
  48. package/get-shit-done/tui/README.md +0 -107
  49. package/get-shit-done/tui/build.js +0 -37
  50. package/get-shit-done/tui/components/ActivityFeed.tsx +0 -126
  51. package/get-shit-done/tui/components/PhaseCard.tsx +0 -86
  52. package/get-shit-done/tui/components/StatsBar.tsx +0 -147
  53. package/get-shit-done/tui/dist/index.js +0 -387
  54. package/get-shit-done/tui/index.tsx +0 -12
  55. package/get-shit-done/tui/package-lock.json +0 -1074
  56. package/get-shit-done/tui/package.json +0 -22
  57. package/get-shit-done/tui/utils/pipeReader.ts +0 -129
  58. package/get-shit-done/workflows/design-system.md +0 -245
  59. package/get-shit-done/workflows/discuss-design.md +0 -330
  60. package/get-shit-done/workflows/execute-plan-auth.md +0 -122
  61. package/get-shit-done/workflows/execute-plan-checkpoints.md +0 -541
package/bin/install.js CHANGED
@@ -21,22 +21,28 @@ const hasGlobal = args.includes('--global') || args.includes('-g');
21
21
  const hasLocal = args.includes('--local') || args.includes('-l');
22
22
  const hasOpencode = args.includes('--opencode');
23
23
  const hasClaude = args.includes('--claude');
24
- const hasBoth = args.includes('--both');
24
+ const hasGemini = args.includes('--gemini');
25
+ const hasBoth = args.includes('--both'); // Legacy flag, keeps working
26
+ const hasAll = args.includes('--all');
25
27
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
26
28
 
27
29
  // Runtime selection - can be set by flags or interactive prompt
28
30
  let selectedRuntimes = [];
29
- if (hasBoth) {
31
+ if (hasAll) {
32
+ selectedRuntimes = ['claude', 'opencode', 'gemini'];
33
+ } else if (hasBoth) {
30
34
  selectedRuntimes = ['claude', 'opencode'];
31
- } else if (hasOpencode) {
32
- selectedRuntimes = ['opencode'];
33
- } else if (hasClaude) {
34
- selectedRuntimes = ['claude'];
35
+ } else {
36
+ if (hasOpencode) selectedRuntimes.push('opencode');
37
+ if (hasClaude) selectedRuntimes.push('claude');
38
+ if (hasGemini) selectedRuntimes.push('gemini');
35
39
  }
36
40
 
37
41
  // Helper to get directory name for a runtime (used for local/project installs)
38
42
  function getDirName(runtime) {
39
- return runtime === 'opencode' ? '.opencode' : '.claude';
43
+ if (runtime === 'opencode') return '.opencode';
44
+ if (runtime === 'gemini') return '.gemini';
45
+ return '.claude';
40
46
  }
41
47
 
42
48
  /**
@@ -66,7 +72,7 @@ function getOpencodeGlobalDir() {
66
72
 
67
73
  /**
68
74
  * Get the global config directory for a runtime
69
- * @param {string} runtime - 'claude' or 'opencode'
75
+ * @param {string} runtime - 'claude', 'opencode', or 'gemini'
70
76
  * @param {string|null} explicitDir - Explicit directory from --config-dir flag
71
77
  */
72
78
  function getGlobalDir(runtime, explicitDir = null) {
@@ -78,6 +84,17 @@ function getGlobalDir(runtime, explicitDir = null) {
78
84
  return getOpencodeGlobalDir();
79
85
  }
80
86
 
87
+ if (runtime === 'gemini') {
88
+ // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
89
+ if (explicitDir) {
90
+ return expandTilde(explicitDir);
91
+ }
92
+ if (process.env.GEMINI_CONFIG_DIR) {
93
+ return expandTilde(process.env.GEMINI_CONFIG_DIR);
94
+ }
95
+ return path.join(os.homedir(), '.gemini');
96
+ }
97
+
81
98
  // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
82
99
  if (explicitDir) {
83
100
  return expandTilde(explicitDir);
@@ -88,18 +105,17 @@ function getGlobalDir(runtime, explicitDir = null) {
88
105
  return path.join(os.homedir(), '.claude');
89
106
  }
90
107
 
91
- const banner = `
92
- ${cyan} ██████╗ ███████╗██████╗
93
- ██╔════╝ ██╔════╝██╔══██╗
94
- ██║ ███╗███████╗██║ ██║
95
- ██║ ██║╚════██║██║ ██║
96
- ╚██████╔╝███████║██████╔╝
97
- ╚═════╝ ╚══════╝╚═════╝${reset}
98
-
99
- Get Shit Done ${dim}v${pkg.version}${reset}
100
- A meta-prompting, context engineering and spec-driven
101
- development system for Claude Code (and opencode) by TÂCHES.
102
- `;
108
+ const banner = '\n' +
109
+ cyan + ' ██████╗ ███████╗██████╗\n' +
110
+ ' ██╔════╝ ██╔════╝██╔══██╗\n' +
111
+ ' ██║ ███╗███████╗██║ ██║\n' +
112
+ ' ██║ ██║╚════██║██║ ██║\n' +
113
+ ' ╚██████╔╝███████║██████╔╝\n' +
114
+ ' ╚═════╝ ╚══════╝╚═════╝' + reset + '\n' +
115
+ '\n' +
116
+ ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' +
117
+ ' A meta-prompting, context engineering and spec-driven\n' +
118
+ ' development system for Claude Code, OpenCode, and Gemini by TÂCHES.\n';
103
119
 
104
120
  // Parse --config-dir argument
105
121
  function parseConfigDirArg() {
@@ -133,49 +149,7 @@ console.log(banner);
133
149
 
134
150
  // Show help if requested
135
151
  if (hasHelp) {
136
- console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]
137
-
138
- ${yellow}Options:${reset}
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
146
- ${cyan}-h, --help${reset} Show this help message
147
- ${cyan}--force-statusline${reset} Replace existing statusline config
148
-
149
- ${yellow}Examples:${reset}
150
- ${dim}# Interactive install (prompts for runtime and location)${reset}
151
- npx get-shit-done-cc
152
-
153
- ${dim}# Install for Claude Code globally${reset}
154
- npx get-shit-done-cc --claude --global
155
-
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
164
-
165
- ${dim}# Install to current project only${reset}
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
173
-
174
- ${yellow}Notes:${reset}
175
- The --config-dir option is useful when you have multiple Claude Code
176
- configurations (e.g., for different subscriptions). It takes priority
177
- over the CLAUDE_CONFIG_DIR environment variable.
178
- `);
152
+ console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Claude Code globally${reset}\n npx get-shit-done-cc --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR environment variables.\n`);
179
153
  process.exit(0);
180
154
  }
181
155
 
@@ -193,9 +167,9 @@ function expandTilde(filePath) {
193
167
  * Build a hook command path using forward slashes for cross-platform compatibility.
194
168
  * On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
195
169
  */
196
- function buildHookCommand(claudeDir, hookName) {
170
+ function buildHookCommand(configDir, hookName) {
197
171
  // Use forward slashes for Node.js compatibility on all platforms
198
- const hooksPath = claudeDir.replace(/\\/g, '/') + '/hooks/' + hookName;
172
+ const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
199
173
  return `node "${hooksPath}"`;
200
174
  }
201
175
 
@@ -371,6 +345,47 @@ function convertClaudeToOpencodeFrontmatter(content) {
371
345
  return `---\n${newFrontmatter}\n---${body}`;
372
346
  }
373
347
 
348
+ /**
349
+ * Convert Claude Code markdown command to Gemini TOML format
350
+ * @param {string} content - Markdown file content with YAML frontmatter
351
+ * @returns {string} - TOML content
352
+ */
353
+ function convertClaudeToGeminiToml(content) {
354
+ // Check if content has frontmatter
355
+ if (!content.startsWith('---')) {
356
+ return `prompt = ${JSON.stringify(content)}\n`;
357
+ }
358
+
359
+ const endIndex = content.indexOf('---', 3);
360
+ if (endIndex === -1) {
361
+ return `prompt = ${JSON.stringify(content)}\n`;
362
+ }
363
+
364
+ const frontmatter = content.substring(3, endIndex).trim();
365
+ const body = content.substring(endIndex + 3).trim();
366
+
367
+ // Extract description from frontmatter
368
+ let description = '';
369
+ const lines = frontmatter.split('\n');
370
+ for (const line of lines) {
371
+ const trimmed = line.trim();
372
+ if (trimmed.startsWith('description:')) {
373
+ description = trimmed.substring(12).trim();
374
+ break;
375
+ }
376
+ }
377
+
378
+ // Construct TOML
379
+ let toml = '';
380
+ if (description) {
381
+ toml += `description = ${JSON.stringify(description)}\n`;
382
+ }
383
+
384
+ toml += `prompt = ${JSON.stringify(body)}\n`;
385
+
386
+ return toml;
387
+ }
388
+
374
389
  /**
375
390
  * Copy commands to a flat structure for OpenCode
376
391
  * OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
@@ -412,17 +427,14 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
412
427
  const baseName = entry.name.replace('.md', '');
413
428
  const destName = `${prefix}-${baseName}.md`;
414
429
  const destPath = path.join(destDir, destName);
415
-
416
- // Read, transform, and write
430
+
417
431
  let content = fs.readFileSync(srcPath, 'utf8');
418
- // Replace path references
419
432
  const claudeDirRegex = /~\/\.claude\//g;
420
433
  const opencodeDirRegex = /~\/\.opencode\//g;
421
434
  content = content.replace(claudeDirRegex, pathPrefix);
422
435
  content = content.replace(opencodeDirRegex, pathPrefix);
423
- // Convert frontmatter for opencode compatibility
424
436
  content = convertClaudeToOpencodeFrontmatter(content);
425
-
437
+
426
438
  fs.writeFileSync(destPath, content);
427
439
  }
428
440
  }
@@ -434,7 +446,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
434
446
  * @param {string} srcDir - Source directory
435
447
  * @param {string} destDir - Destination directory
436
448
  * @param {string} pathPrefix - Path prefix for file references
437
- * @param {string} runtime - Target runtime ('claude' or 'opencode')
449
+ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini')
438
450
  */
439
451
  function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
440
452
  const isOpencode = runtime === 'opencode';
@@ -455,15 +467,24 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
455
467
  if (entry.isDirectory()) {
456
468
  copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime);
457
469
  } else if (entry.name.endsWith('.md')) {
458
- // Replace ~/.claude/ with the appropriate prefix in Markdown files
470
+ // Always replace ~/.claude/ as it is the source of truth in the repo
459
471
  let content = fs.readFileSync(srcPath, 'utf8');
460
- const claudeDirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
472
+ const claudeDirRegex = /~\/\.claude\//g;
461
473
  content = content.replace(claudeDirRegex, pathPrefix);
474
+
462
475
  // Convert frontmatter for opencode compatibility
463
476
  if (isOpencode) {
464
477
  content = convertClaudeToOpencodeFrontmatter(content);
478
+ fs.writeFileSync(destPath, content);
479
+ } else if (runtime === 'gemini') {
480
+ // Convert to TOML for Gemini
481
+ const tomlContent = convertClaudeToGeminiToml(content);
482
+ // Replace extension with .toml
483
+ const tomlPath = destPath.replace(/\.md$/, '.toml');
484
+ fs.writeFileSync(tomlPath, tomlContent);
485
+ } else {
486
+ fs.writeFileSync(destPath, content);
465
487
  }
466
- fs.writeFileSync(destPath, content);
467
488
  } else {
468
489
  fs.copyFileSync(srcPath, destPath);
469
490
  }
@@ -473,14 +494,14 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
473
494
  /**
474
495
  * Clean up orphaned files from previous GSD versions
475
496
  */
476
- function cleanupOrphanedFiles(claudeDir) {
497
+ function cleanupOrphanedFiles(configDir) {
477
498
  const orphanedFiles = [
478
499
  'hooks/gsd-notify.sh', // Removed in v1.6.x
479
500
  'hooks/statusline.js', // Renamed to gsd-statusline.js in v1.9.0
480
501
  ];
481
502
 
482
503
  for (const relPath of orphanedFiles) {
483
- const fullPath = path.join(claudeDir, relPath);
504
+ const fullPath = path.join(configDir, relPath);
484
505
  if (fs.existsSync(fullPath)) {
485
506
  fs.unlinkSync(fullPath);
486
507
  console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
@@ -537,7 +558,7 @@ function cleanupOrphanedHooks(settings) {
537
558
  * Uninstall GSD from the specified directory for a specific runtime
538
559
  * Removes only GSD-specific files/directories, preserves user content
539
560
  * @param {boolean} isGlobal - Whether to uninstall from global or local
540
- * @param {string} runtime - Target runtime ('claude' or 'opencode')
561
+ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini')
541
562
  */
542
563
  function uninstall(isGlobal, runtime = 'claude') {
543
564
  const isOpencode = runtime === 'opencode';
@@ -552,7 +573,10 @@ function uninstall(isGlobal, runtime = 'claude') {
552
573
  ? targetDir.replace(os.homedir(), '~')
553
574
  : targetDir.replace(process.cwd(), '.');
554
575
 
555
- const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
576
+ let runtimeLabel = 'Claude Code';
577
+ if (runtime === 'opencode') runtimeLabel = 'OpenCode';
578
+ if (runtime === 'gemini') runtimeLabel = 'Gemini';
579
+
556
580
  console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
557
581
 
558
582
  // Check if target directory exists
@@ -579,7 +603,7 @@ function uninstall(isGlobal, runtime = 'claude') {
579
603
  console.log(` ${green}✓${reset} Removed GSD commands from command/`);
580
604
  }
581
605
  } else {
582
- // Claude Code: remove commands/gsd/ directory
606
+ // Claude Code & Gemini: remove commands/gsd/ directory
583
607
  const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
584
608
  if (fs.existsSync(gsdCommandsDir)) {
585
609
  fs.rmSync(gsdCommandsDir, { recursive: true });
@@ -616,7 +640,7 @@ function uninstall(isGlobal, runtime = 'claude') {
616
640
  // 4. Remove GSD hooks
617
641
  const hooksDir = path.join(targetDir, 'hooks');
618
642
  if (fs.existsSync(hooksDir)) {
619
- const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh', 'gsd-activity.sh'];
643
+ const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh'];
620
644
  let hookCount = 0;
621
645
  for (const hook of gsdHooks) {
622
646
  const hookPath = path.join(hooksDir, hook);
@@ -660,42 +684,18 @@ function uninstall(isGlobal, runtime = 'claude') {
660
684
  });
661
685
  if (settings.hooks.SessionStart.length < before) {
662
686
  settingsModified = true;
663
- console.log(` ${green}✓${reset} Removed GSD SessionStart hooks from settings`);
687
+ console.log(` ${green}✓${reset} Removed GSD hooks from settings`);
664
688
  }
665
689
  // Clean up empty array
666
690
  if (settings.hooks.SessionStart.length === 0) {
667
691
  delete settings.hooks.SessionStart;
668
692
  }
669
- }
670
-
671
- // Remove GSD activity hook from PostToolUse
672
- if (settings.hooks && settings.hooks.PostToolUse) {
673
- const before = settings.hooks.PostToolUse.length;
674
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
675
- if (entry.hooks && Array.isArray(entry.hooks)) {
676
- // Filter out GSD hooks
677
- const hasGsdHook = entry.hooks.some(h =>
678
- h.command && h.command.includes('gsd-activity')
679
- );
680
- return !hasGsdHook;
681
- }
682
- return true;
683
- });
684
- if (settings.hooks.PostToolUse.length < before) {
685
- settingsModified = true;
686
- console.log(` ${green}✓${reset} Removed GSD PostToolUse hooks from settings`);
687
- }
688
- // Clean up empty array
689
- if (settings.hooks.PostToolUse.length === 0) {
690
- delete settings.hooks.PostToolUse;
693
+ // Clean up empty hooks object
694
+ if (Object.keys(settings.hooks).length === 0) {
695
+ delete settings.hooks;
691
696
  }
692
697
  }
693
698
 
694
- // Clean up empty hooks object
695
- if (settings.hooks && Object.keys(settings.hooks).length === 0) {
696
- delete settings.hooks;
697
- }
698
-
699
699
  if (settingsModified) {
700
700
  writeSettings(settingsPath, settings);
701
701
  removedCount++;
@@ -853,11 +853,12 @@ function verifyFileInstalled(filePath, description) {
853
853
  /**
854
854
  * Install to the specified directory for a specific runtime
855
855
  * @param {boolean} isGlobal - Whether to install globally or locally
856
- * @param {string} runtime - Target runtime ('claude' or 'opencode')
856
+ * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini')
857
857
  */
858
858
  function install(isGlobal, runtime = 'claude') {
859
859
  const isOpencode = runtime === 'opencode';
860
- const dirName = getDirName(runtime); // .opencode or .claude (for local installs)
860
+ const isGemini = runtime === 'gemini';
861
+ const dirName = getDirName(runtime);
861
862
  const src = path.join(__dirname, '..');
862
863
 
863
864
  // Get the target directory based on runtime and install type
@@ -870,13 +871,16 @@ function install(isGlobal, runtime = 'claude') {
870
871
  : targetDir.replace(process.cwd(), '.');
871
872
 
872
873
  // Path prefix for file references in markdown content
873
- // For global installs: use full path (necessary when config dir is customized)
874
- // For local installs: use relative ./.opencode/ or ./.claude/
874
+ // For global installs: use full path
875
+ // For local installs: use relative
875
876
  const pathPrefix = isGlobal
876
877
  ? `${targetDir}/`
877
878
  : `./${dirName}/`;
878
879
 
879
- const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
880
+ let runtimeLabel = 'Claude Code';
881
+ if (isOpencode) runtimeLabel = 'OpenCode';
882
+ if (isGemini) runtimeLabel = 'Gemini';
883
+
880
884
  console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
881
885
 
882
886
  // Track installation failures
@@ -885,8 +889,8 @@ function install(isGlobal, runtime = 'claude') {
885
889
  // Clean up orphaned files from previous versions
886
890
  cleanupOrphanedFiles(targetDir);
887
891
 
888
- // OpenCode uses 'command/' (singular) with flat structure: command/gsd-help.md
889
- // Claude Code uses 'commands/' (plural) with nested structure: commands/gsd/help.md
892
+ // OpenCode uses 'command/' (singular) with flat structure
893
+ // Claude Code & Gemini use 'commands/' (plural) with nested structure
890
894
  if (isOpencode) {
891
895
  // OpenCode: flat structure in command/ directory
892
896
  const commandDir = path.join(targetDir, 'command');
@@ -902,7 +906,7 @@ function install(isGlobal, runtime = 'claude') {
902
906
  failures.push('command/gsd-*');
903
907
  }
904
908
  } else {
905
- // Claude Code: nested structure in commands/ directory
909
+ // Claude Code & Gemini: nested structure in commands/ directory
906
910
  const commandsDir = path.join(targetDir, 'commands');
907
911
  fs.mkdirSync(commandsDir, { recursive: true });
908
912
 
@@ -926,8 +930,7 @@ function install(isGlobal, runtime = 'claude') {
926
930
  failures.push('get-shit-done');
927
931
  }
928
932
 
929
- // Copy agents to agents directory (subagents must be at root level)
930
- // Only delete gsd-*.md files to preserve user's custom agents
933
+ // Copy agents to agents directory
931
934
  const agentsSrc = path.join(src, 'agents');
932
935
  if (fs.existsSync(agentsSrc)) {
933
936
  const agentsDest = path.join(targetDir, 'agents');
@@ -942,12 +945,13 @@ function install(isGlobal, runtime = 'claude') {
942
945
  }
943
946
  }
944
947
 
945
- // Copy new agents (don't use copyWithPathReplacement which would wipe the folder)
948
+ // Copy new agents
946
949
  const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
947
950
  for (const entry of agentEntries) {
948
951
  if (entry.isFile() && entry.name.endsWith('.md')) {
949
952
  let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
950
- const dirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
953
+ // Always replace ~/.claude/ as it is the source of truth in the repo
954
+ const dirRegex = /~\/\.claude\//g;
951
955
  content = content.replace(dirRegex, pathPrefix);
952
956
  // Convert frontmatter for opencode compatibility
953
957
  if (isOpencode) {
@@ -975,7 +979,7 @@ function install(isGlobal, runtime = 'claude') {
975
979
  }
976
980
  }
977
981
 
978
- // Write VERSION file for whats-new command
982
+ // Write VERSION file
979
983
  const versionDest = path.join(targetDir, 'get-shit-done', 'VERSION');
980
984
  fs.writeFileSync(versionDest, pkg.version);
981
985
  if (verifyFileInstalled(versionDest, 'VERSION')) {
@@ -992,7 +996,6 @@ function install(isGlobal, runtime = 'claude') {
992
996
  const hookEntries = fs.readdirSync(hooksSrc);
993
997
  for (const entry of hookEntries) {
994
998
  const srcFile = path.join(hooksSrc, entry);
995
- // Only copy files, not directories
996
999
  if (fs.statSync(srcFile).isFile()) {
997
1000
  const destFile = path.join(hooksDest, entry);
998
1001
  fs.copyFileSync(srcFile, destFile);
@@ -1005,30 +1008,13 @@ function install(isGlobal, runtime = 'claude') {
1005
1008
  }
1006
1009
  }
1007
1010
 
1008
- // Copy GSD activity hook for autopilot real-time display
1009
- const activityHookSrc = path.join(src, 'hooks', 'gsd-activity.sh');
1010
- if (fs.existsSync(activityHookSrc)) {
1011
- const hooksDest = path.join(targetDir, 'hooks');
1012
- fs.mkdirSync(hooksDest, { recursive: true });
1013
- const activityHookDest = path.join(hooksDest, 'gsd-activity.sh');
1014
- fs.copyFileSync(activityHookSrc, activityHookDest);
1015
- // Make executable on Unix systems
1016
- try {
1017
- fs.chmodSync(activityHookDest, 0o755);
1018
- } catch (e) {
1019
- // Windows doesn't support chmod
1020
- }
1021
- console.log(` ${green}✓${reset} Installed gsd-activity.sh hook`);
1022
- }
1023
-
1024
- // If critical components failed, exit with error
1025
1011
  if (failures.length > 0) {
1026
1012
  console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
1027
- console.error(` Try running directly: node ~/.npm/_npx/*/node_modules/get-shit-done-cc/bin/install.js --global\n`);
1028
1013
  process.exit(1);
1029
1014
  }
1030
1015
 
1031
1016
  // Configure statusline and hooks in settings.json
1017
+ // Gemini shares same hook system as Claude Code for now
1032
1018
  const settingsPath = path.join(targetDir, 'settings.json');
1033
1019
  const settings = cleanupOrphanedHooks(readSettings(settingsPath));
1034
1020
  const statuslineCommand = isGlobal
@@ -1038,7 +1024,7 @@ function install(isGlobal, runtime = 'claude') {
1038
1024
  ? buildHookCommand(targetDir, 'gsd-check-update.js')
1039
1025
  : 'node ' + dirName + '/hooks/gsd-check-update.js';
1040
1026
 
1041
- // Configure SessionStart hook for update checking (skip for opencode - different hook system)
1027
+ // Configure SessionStart hook for update checking (skip for opencode)
1042
1028
  if (!isOpencode) {
1043
1029
  if (!settings.hooks) {
1044
1030
  settings.hooks = {};
@@ -1047,7 +1033,6 @@ function install(isGlobal, runtime = 'claude') {
1047
1033
  settings.hooks.SessionStart = [];
1048
1034
  }
1049
1035
 
1050
- // Check if GSD update hook already exists
1051
1036
  const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
1052
1037
  entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update'))
1053
1038
  );
@@ -1063,53 +1048,6 @@ function install(isGlobal, runtime = 'claude') {
1063
1048
  });
1064
1049
  console.log(` ${green}✓${reset} Configured update check hook`);
1065
1050
  }
1066
-
1067
- // Configure PostToolUse hook for autopilot activity display
1068
- if (!settings.hooks.PostToolUse) {
1069
- settings.hooks.PostToolUse = [];
1070
- }
1071
-
1072
- // Build activity hook command path
1073
- const activityHookCommand = isGlobal
1074
- ? `bash "${targetDir.replace(/\\/g, '/')}/hooks/gsd-activity.sh"`
1075
- : `bash ${dirName}/hooks/gsd-activity.sh`;
1076
-
1077
- // Check if GSD activity hook already exists (handles both old and new format)
1078
- let hasGsdActivityHook = false;
1079
-
1080
- // Remove old-format GSD activity hooks if present
1081
- const beforeCount = settings.hooks.PostToolUse.length;
1082
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
1083
- // Check if this is a new-format GSD hook (with hooks array)
1084
- if (entry.hooks && Array.isArray(entry.hooks)) {
1085
- const isGsdHook = entry.hooks.some(h => h.command && h.command.includes('gsd-activity'));
1086
- if (isGsdHook) {
1087
- hasGsdActivityHook = true;
1088
- return true; // Keep new-format hooks
1089
- }
1090
- }
1091
- // Check if this is an old-format GSD hook (with direct command field)
1092
- const isOldGsdHook = entry.command && entry.command.includes('gsd-activity');
1093
- if (isOldGsdHook) {
1094
- return false; // Remove old-format hooks
1095
- }
1096
- // Keep non-GSD hooks
1097
- return true;
1098
- });
1099
-
1100
- // Add new-format hook if not found
1101
- if (!hasGsdActivityHook) {
1102
- settings.hooks.PostToolUse.push({
1103
- matcher: "Task|Write|Edit|Read|Bash|TodoWrite",
1104
- hooks: [
1105
- {
1106
- type: "command",
1107
- command: activityHookCommand
1108
- }
1109
- ]
1110
- });
1111
- console.log(` ${green}✓${reset} Configured autopilot activity hook`);
1112
- }
1113
1051
  }
1114
1052
 
1115
1053
  return { settingsPath, settings, statuslineCommand, runtime };
@@ -1117,11 +1055,6 @@ function install(isGlobal, runtime = 'claude') {
1117
1055
 
1118
1056
  /**
1119
1057
  * Apply statusline config, then print completion message
1120
- * @param {string} settingsPath - Path to settings.json
1121
- * @param {object} settings - Settings object
1122
- * @param {string} statuslineCommand - Statusline command
1123
- * @param {boolean} shouldInstallStatusline - Whether to install statusline
1124
- * @param {string} runtime - Target runtime ('claude' or 'opencode')
1125
1058
  */
1126
1059
  function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude') {
1127
1060
  const isOpencode = runtime === 'opencode';
@@ -1134,15 +1067,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
1134
1067
  console.log(` ${green}✓${reset} Configured statusline`);
1135
1068
  }
1136
1069
 
1137
- // Always write settings (hooks were already configured in install())
1070
+ // Always write settings
1138
1071
  writeSettings(settingsPath, settings);
1139
1072
 
1140
- // Configure OpenCode permissions if needed
1073
+ // Configure OpenCode permissions
1141
1074
  if (isOpencode) {
1142
1075
  configureOpencodePermissions();
1143
1076
  }
1144
1077
 
1145
- const program = isOpencode ? 'OpenCode' : 'Claude Code';
1078
+ let program = 'Claude Code';
1079
+ if (runtime === 'opencode') program = 'OpenCode';
1080
+ if (runtime === 'gemini') program = 'Gemini';
1081
+
1146
1082
  const command = isOpencode ? '/gsd-help' : '/gsd:help';
1147
1083
  console.log(`
1148
1084
  ${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
@@ -1157,19 +1093,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
1157
1093
  function handleStatusline(settings, isInteractive, callback) {
1158
1094
  const hasExisting = settings.statusLine != null;
1159
1095
 
1160
- // No existing statusline - just install it
1161
1096
  if (!hasExisting) {
1162
1097
  callback(true);
1163
1098
  return;
1164
1099
  }
1165
1100
 
1166
- // Has existing and --force-statusline flag
1167
1101
  if (forceStatusline) {
1168
1102
  callback(true);
1169
1103
  return;
1170
1104
  }
1171
1105
 
1172
- // Has existing, non-interactive mode - skip
1173
1106
  if (!isInteractive) {
1174
1107
  console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
1175
1108
  console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
@@ -1177,7 +1110,6 @@ function handleStatusline(settings, isInteractive, callback) {
1177
1110
  return;
1178
1111
  }
1179
1112
 
1180
- // Has existing, interactive mode - prompt user
1181
1113
  const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
1182
1114
 
1183
1115
  const rl = readline.createInterface({
@@ -1186,8 +1118,7 @@ function handleStatusline(settings, isInteractive, callback) {
1186
1118
  });
1187
1119
 
1188
1120
  console.log(`
1189
- ${yellow}⚠${reset} Existing statusline detected
1190
-
1121
+ ${yellow}⚠${reset} Existing statusline detected\n
1191
1122
  Your current statusline:
1192
1123
  ${dim}command: ${existingCmd}${reset}
1193
1124
 
@@ -1208,8 +1139,7 @@ function handleStatusline(settings, isInteractive, callback) {
1208
1139
  }
1209
1140
 
1210
1141
  /**
1211
- * Prompt for runtime selection (Claude Code / OpenCode / Both)
1212
- * @param {function} callback - Called with array of selected runtimes
1142
+ * Prompt for runtime selection
1213
1143
  */
1214
1144
  function promptRuntime(callback) {
1215
1145
  const rl = readline.createInterface({
@@ -1227,19 +1157,20 @@ function promptRuntime(callback) {
1227
1157
  }
1228
1158
  });
1229
1159
 
1230
- console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}
1231
-
1232
- ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
1160
+ console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
1233
1161
  ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
1234
- ${cyan}3${reset}) Both
1162
+ ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
1163
+ ${cyan}4${reset}) All
1235
1164
  `);
1236
1165
 
1237
1166
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1238
1167
  answered = true;
1239
1168
  rl.close();
1240
1169
  const choice = answer.trim() || '1';
1241
- if (choice === '3') {
1242
- callback(['claude', 'opencode']);
1170
+ if (choice === '4') {
1171
+ callback(['claude', 'opencode', 'gemini']);
1172
+ } else if (choice === '3') {
1173
+ callback(['gemini']);
1243
1174
  } else if (choice === '2') {
1244
1175
  callback(['opencode']);
1245
1176
  } else {
@@ -1250,11 +1181,8 @@ function promptRuntime(callback) {
1250
1181
 
1251
1182
  /**
1252
1183
  * Prompt for install location
1253
- * @param {string[]} runtimes - Array of runtimes to install for
1254
1184
  */
1255
1185
  function promptLocation(runtimes) {
1256
- // Check if stdin is a TTY - if not, fall back to global install
1257
- // This handles npx execution in environments like WSL2 where stdin may not be properly connected
1258
1186
  if (!process.stdin.isTTY) {
1259
1187
  console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
1260
1188
  installAllRuntimes(runtimes, true, false);
@@ -1266,10 +1194,8 @@ function promptLocation(runtimes) {
1266
1194
  output: process.stdout
1267
1195
  });
1268
1196
 
1269
- // Track whether we've processed the answer to prevent double-execution
1270
1197
  let answered = false;
1271
1198
 
1272
- // Handle readline close event (Ctrl+C, Escape, etc.) - cancel installation
1273
1199
  rl.on('close', () => {
1274
1200
  if (!answered) {
1275
1201
  answered = true;
@@ -1278,18 +1204,14 @@ function promptLocation(runtimes) {
1278
1204
  }
1279
1205
  });
1280
1206
 
1281
- // Show paths for selected runtimes
1282
1207
  const pathExamples = runtimes.map(r => {
1283
- // Use the proper global directory function for each runtime
1284
1208
  const globalPath = getGlobalDir(r, explicitConfigDir);
1285
1209
  return globalPath.replace(os.homedir(), '~');
1286
1210
  }).join(', ');
1287
1211
 
1288
1212
  const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
1289
1213
 
1290
- console.log(` ${yellow}Where would you like to install?${reset}
1291
-
1292
- ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
1214
+ console.log(` ${yellow}Where would you like to install?${reset}\n\n ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
1293
1215
  ${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
1294
1216
  `);
1295
1217
 
@@ -1304,9 +1226,6 @@ function promptLocation(runtimes) {
1304
1226
 
1305
1227
  /**
1306
1228
  * Install GSD for all selected runtimes
1307
- * @param {string[]} runtimes - Array of runtimes to install for
1308
- * @param {boolean} isGlobal - Whether to install globally
1309
- * @param {boolean} isInteractive - Whether running interactively
1310
1229
  */
1311
1230
  function installAllRuntimes(runtimes, isGlobal, isInteractive) {
1312
1231
  const results = [];
@@ -1316,14 +1235,25 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
1316
1235
  results.push(result);
1317
1236
  }
1318
1237
 
1319
- // Handle statusline for Claude Code only (OpenCode uses themes)
1238
+ // Handle statusline for Claude & Gemini (OpenCode uses themes)
1320
1239
  const claudeResult = results.find(r => r.runtime === 'claude');
1240
+ const geminiResult = results.find(r => r.runtime === 'gemini');
1321
1241
 
1322
- if (claudeResult) {
1323
- handleStatusline(claudeResult.settings, isInteractive, (shouldInstallStatusline) => {
1324
- finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude');
1325
-
1326
- // Finish OpenCode install if present
1242
+ // Logic: if both are present, ask once if interactive? Or ask for each?
1243
+ // Simpler: Ask once and apply to both if applicable.
1244
+
1245
+ if (claudeResult || geminiResult) {
1246
+ // Use whichever settings exist to check for existing statusline
1247
+ const primaryResult = claudeResult || geminiResult;
1248
+
1249
+ handleStatusline(primaryResult.settings, isInteractive, (shouldInstallStatusline) => {
1250
+ if (claudeResult) {
1251
+ finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude');
1252
+ }
1253
+ if (geminiResult) {
1254
+ finishInstall(geminiResult.settingsPath, geminiResult.settings, geminiResult.statuslineCommand, shouldInstallStatusline, 'gemini');
1255
+ }
1256
+
1327
1257
  const opencodeResult = results.find(r => r.runtime === 'opencode');
1328
1258
  if (opencodeResult) {
1329
1259
  finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
@@ -1336,7 +1266,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
1336
1266
  }
1337
1267
  }
1338
1268
 
1339
- // Main
1269
+ // Main logic
1340
1270
  if (hasGlobal && hasLocal) {
1341
1271
  console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
1342
1272
  process.exit(1);
@@ -1344,10 +1274,8 @@ if (hasGlobal && hasLocal) {
1344
1274
  console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
1345
1275
  process.exit(1);
1346
1276
  } else if (hasUninstall) {
1347
- // Uninstall mode
1348
1277
  if (!hasGlobal && !hasLocal) {
1349
1278
  console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
1350
- console.error(` Example: npx get-shit-done-cc --claude --global --uninstall`);
1351
1279
  process.exit(1);
1352
1280
  }
1353
1281
  const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
@@ -1355,19 +1283,16 @@ if (hasGlobal && hasLocal) {
1355
1283
  uninstall(hasGlobal, runtime);
1356
1284
  }
1357
1285
  } else if (selectedRuntimes.length > 0) {
1358
- // Non-interactive: runtime specified via flags
1359
1286
  if (!hasGlobal && !hasLocal) {
1360
- // Need location but runtime is specified - prompt for location only
1361
1287
  promptLocation(selectedRuntimes);
1362
1288
  } else {
1363
- // Both runtime and location specified via flags
1364
1289
  installAllRuntimes(selectedRuntimes, hasGlobal, false);
1365
1290
  }
1366
1291
  } else if (hasGlobal || hasLocal) {
1367
- // Location specified but no runtime - default to Claude Code
1292
+ // Default to Claude if no runtime specified but location is
1368
1293
  installAllRuntimes(['claude'], hasGlobal, false);
1369
1294
  } else {
1370
- // Fully interactive: prompt for runtime, then location
1295
+ // Interactive
1371
1296
  if (!process.stdin.isTTY) {
1372
1297
  console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
1373
1298
  installAllRuntimes(['claude'], true, false);