ragarciaruben 1.20.19 → 1.20.21

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 (91) hide show
  1. package/.github/agents/product-owner.agent.md +98 -0
  2. package/.github/agents/verifier.agent.md +3 -0
  3. package/.github/prompts/break-down-epic.prompt.md +125 -0
  4. package/.github/prompts/create-tickets.prompt.md +93 -0
  5. package/.github/prompts/refine-backlog.prompt.md +88 -0
  6. package/bin/install.js +406 -1969
  7. package/get-shit-done/templates/AGENTS.md +83 -0
  8. package/get-shit-done/templates/opencode/agents/executor.md +61 -0
  9. package/get-shit-done/templates/opencode/agents/planner.md +77 -0
  10. package/get-shit-done/templates/opencode/agents/product-owner.md +65 -0
  11. package/{.github/copilot-context/agents/verifier.agent.md → get-shit-done/templates/opencode/agents/verifier.md} +12 -35
  12. package/get-shit-done/templates/opencode/commands/break-down-epic.md +120 -0
  13. package/get-shit-done/templates/opencode/commands/create-tickets.md +75 -0
  14. package/{.github/copilot-context/prompts/execute-phase.prompt.md → get-shit-done/templates/opencode/commands/execute-phase.md} +5 -33
  15. package/{.github/copilot-context/prompts/map-codebase.prompt.md → get-shit-done/templates/opencode/commands/map-codebase.md} +12 -41
  16. package/{.github/copilot-context/prompts/new-project.prompt.md → get-shit-done/templates/opencode/commands/new-project.md} +17 -33
  17. package/{.github/copilot-context/prompts/pause-work.prompt.md → get-shit-done/templates/opencode/commands/pause-work.md} +6 -19
  18. package/{.github/copilot-context/prompts/plan-phase.prompt.md → get-shit-done/templates/opencode/commands/plan-phase.md} +4 -27
  19. package/{.github/copilot-context/prompts/progress.prompt.md → get-shit-done/templates/opencode/commands/progress.md} +1 -4
  20. package/{.github/copilot-context/prompts/redefine-roadmap.prompt.md → get-shit-done/templates/opencode/commands/redefine-roadmap.md} +8 -21
  21. package/get-shit-done/templates/opencode/commands/refine-backlog.md +77 -0
  22. package/{.github/copilot-context/prompts/resume-work.prompt.md → get-shit-done/templates/opencode/commands/resume-work.md} +2 -13
  23. package/get-shit-done/templates/opencode/commands/set-profile.md +65 -0
  24. package/{.github/copilot-context/prompts/sync-instructions.prompt.md → get-shit-done/templates/opencode/commands/sync-instructions.md} +9 -13
  25. package/{.github/copilot-context/prompts/sync-jira.prompt.md → get-shit-done/templates/opencode/commands/sync-jira.md} +5 -17
  26. package/{.github/copilot-context/prompts/verify-work.prompt.md → get-shit-done/templates/opencode/commands/verify-work.md} +5 -33
  27. package/get-shit-done/templates/opencode.json +15 -0
  28. package/package.json +7 -17
  29. package/.github/copilot-context/README.md +0 -556
  30. package/.github/copilot-context/agents/executor.agent.md +0 -84
  31. package/.github/copilot-context/agents/planner.agent.md +0 -96
  32. package/.github/copilot-context/hooks/hooks.json +0 -11
  33. package/.github/copilot-context/hooks/inject-context.js +0 -107
  34. package/.github/copilot-context/instructions/architecture.instructions.md +0 -33
  35. package/.github/copilot-context/instructions/concerns.instructions.md +0 -30
  36. package/.github/copilot-context/instructions/conventions.instructions.md +0 -25
  37. package/.github/copilot-context/instructions/integrations.instructions.md +0 -30
  38. package/.github/copilot-context/instructions/stack.instructions.md +0 -30
  39. package/.github/copilot-context/instructions/structure.instructions.md +0 -32
  40. package/.github/copilot-context/instructions/testing.instructions.md +0 -25
  41. package/.github/copilot-context/skills/map-codebase/SKILL.md +0 -49
  42. package/.github/copilot-context/skills/project-history/SKILL.md +0 -46
  43. package/.vscode/settings.json +0 -9
  44. package/agents/gsd-codebase-mapper.md +0 -764
  45. package/agents/gsd-debugger.md +0 -1246
  46. package/agents/gsd-executor.md +0 -469
  47. package/agents/gsd-integration-checker.md +0 -443
  48. package/agents/gsd-phase-researcher.md +0 -546
  49. package/agents/gsd-plan-checker.md +0 -690
  50. package/agents/gsd-planner.md +0 -1275
  51. package/agents/gsd-project-researcher.md +0 -621
  52. package/agents/gsd-research-synthesizer.md +0 -239
  53. package/agents/gsd-roadmapper.md +0 -642
  54. package/agents/gsd-verifier.md +0 -573
  55. package/bin/setup-copilot-context.js +0 -245
  56. package/commands/gsd/add-phase.md +0 -43
  57. package/commands/gsd/add-tests.md +0 -41
  58. package/commands/gsd/add-todo.md +0 -47
  59. package/commands/gsd/audit-milestone.md +0 -36
  60. package/commands/gsd/check-todos.md +0 -45
  61. package/commands/gsd/cleanup.md +0 -18
  62. package/commands/gsd/complete-milestone.md +0 -136
  63. package/commands/gsd/debug.md +0 -167
  64. package/commands/gsd/discuss-phase.md +0 -83
  65. package/commands/gsd/execute-phase.md +0 -41
  66. package/commands/gsd/health.md +0 -22
  67. package/commands/gsd/help.md +0 -22
  68. package/commands/gsd/insert-phase.md +0 -32
  69. package/commands/gsd/join-discord.md +0 -18
  70. package/commands/gsd/list-phase-assumptions.md +0 -46
  71. package/commands/gsd/map-codebase.md +0 -71
  72. package/commands/gsd/new-milestone.md +0 -44
  73. package/commands/gsd/new-project.md +0 -42
  74. package/commands/gsd/new-project.md.bak +0 -1041
  75. package/commands/gsd/pause-work.md +0 -38
  76. package/commands/gsd/plan-milestone-gaps.md +0 -34
  77. package/commands/gsd/plan-phase.md +0 -45
  78. package/commands/gsd/progress.md +0 -24
  79. package/commands/gsd/quick.md +0 -41
  80. package/commands/gsd/reapply-patches.md +0 -110
  81. package/commands/gsd/remove-phase.md +0 -31
  82. package/commands/gsd/research-phase.md +0 -189
  83. package/commands/gsd/resume-work.md +0 -40
  84. package/commands/gsd/set-profile.md +0 -34
  85. package/commands/gsd/settings.md +0 -36
  86. package/commands/gsd/update.md +0 -37
  87. package/commands/gsd/verify-work.md +0 -38
  88. package/hooks/dist/gsd-check-update.js +0 -62
  89. package/hooks/dist/gsd-context-monitor.js +0 -122
  90. package/hooks/dist/gsd-statusline.js +0 -108
  91. package/scripts/build-hooks.js +0 -43
package/bin/install.js CHANGED
@@ -1,1945 +1,466 @@
1
1
  #!/usr/bin/env node
2
-
3
- const fs = require('fs');
2
+ 'use strict';
3
+
4
+ /**
5
+ * install.js — GSD Context Installer
6
+ *
7
+ * Installs the GSD (Get Shit Done) context engineering system into any project.
8
+ *
9
+ * Two targets:
10
+ * --copilot GitHub Copilot (VS Code)
11
+ * --opencode OpenCode (terminal)
12
+ *
13
+ * What gets installed:
14
+ *
15
+ * Copilot:
16
+ * .github/copilot-instructions.md — always-on project digest
17
+ * .github/prompts/ — slash commands
18
+ * .github/instructions/ — conditional context (applyTo globs)
19
+ * .github/agents/ — multi-step agents
20
+ * .planning/ — source-of-truth templates
21
+ *
22
+ * OpenCode:
23
+ * AGENTS.md — always-on project rules
24
+ * .opencode/agents/ — agent definitions
25
+ * .opencode/commands/ — slash commands
26
+ * .planning/ — source-of-truth templates
27
+ * opencode.json (merged) — instructions + permissions
28
+ */
29
+
30
+ const fs = require('fs');
4
31
  const path = require('path');
5
- const os = require('os');
6
32
  const readline = require('readline');
7
- const crypto = require('crypto');
8
-
9
- // Colors
10
- const cyan = '\x1b[36m';
11
- const green = '\x1b[32m';
12
- const yellow = '\x1b[33m';
13
- const dim = '\x1b[2m';
14
- const reset = '\x1b[0m';
15
-
16
- // Get version from package.json
17
- const pkg = require('../package.json');
18
-
19
- // Parse args
20
- const args = process.argv.slice(2);
21
- if (args[0] === 'copilot-context') { require('./setup-copilot-context'); return; }
22
- const hasGlobal = args.includes('--global') || args.includes('-g');
23
- const hasLocal = args.includes('--local') || args.includes('-l');
24
- const hasOpencode = args.includes('--opencode');
25
- const hasClaude = args.includes('--claude');
26
- const hasGemini = args.includes('--gemini');
27
- const hasCodex = args.includes('--codex');
28
- const hasCopilot = args.includes('--copilot');
29
- const hasBoth = args.includes('--both'); // Legacy flag, keeps working
30
- const hasAll = args.includes('--all');
31
- const hasUninstall = args.includes('--uninstall') || args.includes('-u');
32
-
33
- // Runtime selection - can be set by flags or interactive prompt
34
- let selectedRuntimes = [];
35
- if (hasCopilot) {
36
- require('./setup-copilot-context');
37
- return;
38
- } else if (hasAll) {
39
- selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex'];
40
- } else if (hasBoth) {
41
- selectedRuntimes = ['claude', 'opencode'];
42
- } else {
43
- if (hasOpencode) selectedRuntimes.push('opencode');
44
- if (hasClaude) selectedRuntimes.push('claude');
45
- if (hasGemini) selectedRuntimes.push('gemini');
46
- if (hasCodex) selectedRuntimes.push('codex');
47
- }
48
-
49
- // Helper to get directory name for a runtime (used for local/project installs)
50
- function getDirName(runtime) {
51
- if (runtime === 'opencode') return '.opencode';
52
- if (runtime === 'gemini') return '.gemini';
53
- if (runtime === 'codex') return '.codex';
54
- return '.claude';
55
- }
56
-
57
- /**
58
- * Get the config directory path relative to home directory for a runtime
59
- * Used for templating hooks that use path.join(homeDir, '<configDir>', ...)
60
- * @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
61
- * @param {boolean} isGlobal - Whether this is a global install
62
- */
63
- function getConfigDirFromHome(runtime, isGlobal) {
64
- if (!isGlobal) {
65
- // Local installs use the same dir name pattern
66
- return `'${getDirName(runtime)}'`;
67
- }
68
- // Global installs - OpenCode uses XDG path structure
69
- if (runtime === 'opencode') {
70
- // OpenCode: ~/.config/opencode -> '.config', 'opencode'
71
- // Return as comma-separated for path.join() replacement
72
- return "'.config', 'opencode'";
73
- }
74
- if (runtime === 'gemini') return "'.gemini'";
75
- if (runtime === 'codex') return "'.codex'";
76
- return "'.claude'";
77
- }
78
33
 
79
- /**
80
- * Get the global config directory for OpenCode
81
- * OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
82
- * Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
83
- */
84
- function getOpencodeGlobalDir() {
85
- // 1. Explicit OPENCODE_CONFIG_DIR env var
86
- if (process.env.OPENCODE_CONFIG_DIR) {
87
- return expandTilde(process.env.OPENCODE_CONFIG_DIR);
88
- }
89
-
90
- // 2. OPENCODE_CONFIG env var (use its directory)
91
- if (process.env.OPENCODE_CONFIG) {
92
- return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
93
- }
94
-
95
- // 3. XDG_CONFIG_HOME/opencode
96
- if (process.env.XDG_CONFIG_HOME) {
97
- return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
98
- }
99
-
100
- // 4. Default: ~/.config/opencode (XDG default)
101
- return path.join(os.homedir(), '.config', 'opencode');
102
- }
34
+ // ─── Colours ─────────────────────────────────────────────────────────────────
35
+
36
+ const hasColor = process.stdout.isTTY && (
37
+ process.platform !== 'win32' ||
38
+ !!process.env.WT_SESSION ||
39
+ process.env.TERM_PROGRAM === 'vscode' ||
40
+ /^xterm|^screen/.test(process.env.TERM || '')
41
+ );
42
+
43
+ const c = {
44
+ bold: s => hasColor ? `\x1b[1m${s}\x1b[0m` : s,
45
+ green: s => hasColor ? `\x1b[32m${s}\x1b[0m` : s,
46
+ blue: s => hasColor ? `\x1b[34m${s}\x1b[0m` : s,
47
+ cyan: s => hasColor ? `\x1b[36m${s}\x1b[0m` : s,
48
+ yellow: s => hasColor ? `\x1b[33m${s}\x1b[0m` : s,
49
+ dim: s => hasColor ? `\x1b[2m${s}\x1b[0m` : s,
50
+ red: s => hasColor ? `\x1b[31m${s}\x1b[0m` : s,
51
+ };
103
52
 
104
- /**
105
- * Get the global config directory for a runtime
106
- * @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
107
- * @param {string|null} explicitDir - Explicit directory from --config-dir flag
108
- */
109
- function getGlobalDir(runtime, explicitDir = null) {
110
- if (runtime === 'opencode') {
111
- // For OpenCode, --config-dir overrides env vars
112
- if (explicitDir) {
113
- return expandTilde(explicitDir);
114
- }
115
- return getOpencodeGlobalDir();
116
- }
117
-
118
- if (runtime === 'gemini') {
119
- // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini
120
- if (explicitDir) {
121
- return expandTilde(explicitDir);
122
- }
123
- if (process.env.GEMINI_CONFIG_DIR) {
124
- return expandTilde(process.env.GEMINI_CONFIG_DIR);
125
- }
126
- return path.join(os.homedir(), '.gemini');
127
- }
53
+ // ─── Version ─────────────────────────────────────────────────────────────────
128
54
 
129
- if (runtime === 'codex') {
130
- // Codex: --config-dir > CODEX_HOME > ~/.codex
131
- if (explicitDir) {
132
- return expandTilde(explicitDir);
133
- }
134
- if (process.env.CODEX_HOME) {
135
- return expandTilde(process.env.CODEX_HOME);
136
- }
137
- return path.join(os.homedir(), '.codex');
138
- }
139
-
140
- // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
141
- if (explicitDir) {
142
- return expandTilde(explicitDir);
143
- }
144
- if (process.env.CLAUDE_CONFIG_DIR) {
145
- return expandTilde(process.env.CLAUDE_CONFIG_DIR);
146
- }
147
- return path.join(os.homedir(), '.claude');
148
- }
55
+ const pkg = require('../package.json');
149
56
 
150
- const banner = '\n' +
151
- cyan + ' ██████╗ ███████╗██████╗\n' +
152
- ' ██╔════╝ ██╔════╝██╔══██╗\n' +
153
- ' ██║ ███╗███████╗██║ ██║\n' +
154
- ' ██║ ██║╚════██║██║ ██║\n' +
155
- ' ╚██████╔╝███████║██████╔╝\n' +
156
- ' ╚═════╝ ╚══════╝╚═════╝' + reset + '\n' +
57
+ const banner =
157
58
  '\n' +
158
- ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' +
159
- ' A meta-prompting, context engineering and spec-driven\n' +
160
- ' development system for Claude Code, OpenCode, Gemini, and Codex by TÂCHES.\n';
161
-
162
- // Parse --config-dir argument
163
- function parseConfigDirArg() {
164
- const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
165
- if (configDirIndex !== -1) {
166
- const nextArg = args[configDirIndex + 1];
167
- // Error if --config-dir is provided without a value or next arg is another flag
168
- if (!nextArg || nextArg.startsWith('-')) {
169
- console.error(` ${yellow}--config-dir requires a path argument${reset}`);
170
- process.exit(1);
171
- }
172
- return nextArg;
173
- }
174
- // Also handle --config-dir=value format
175
- const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
176
- if (configDirArg) {
177
- const value = configDirArg.split('=')[1];
178
- if (!value) {
179
- console.error(` ${yellow}--config-dir requires a non-empty path${reset}`);
180
- process.exit(1);
181
- }
182
- return value;
183
- }
184
- return null;
185
- }
186
- const explicitConfigDir = parseConfigDirArg();
187
- const hasHelp = args.includes('--help') || args.includes('-h');
188
- const forceStatusline = args.includes('--force-statusline');
189
-
190
- console.log(banner);
191
-
192
- // Show help if requested
193
- if (hasHelp) {
194
- 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}--codex${reset} Install for Codex 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 Codex globally${reset}\n npx get-shit-done-cc --codex --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 --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --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 / CODEX_HOME environment variables.\n`);
195
- process.exit(0);
196
- }
197
-
198
- /**
199
- * Expand ~ to home directory (shell doesn't expand in env vars passed to node)
200
- */
201
- function expandTilde(filePath) {
202
- if (filePath && filePath.startsWith('~/')) {
203
- return path.join(os.homedir(), filePath.slice(2));
204
- }
205
- return filePath;
206
- }
207
-
208
- /**
209
- * Build a hook command path using forward slashes for cross-platform compatibility.
210
- * On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
211
- */
212
- function buildHookCommand(configDir, hookName) {
213
- // Use forward slashes for Node.js compatibility on all platforms
214
- const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
215
- return `node "${hooksPath}"`;
216
- }
217
-
218
- /**
219
- * Read and parse settings.json, returning empty object if it doesn't exist
220
- */
221
- function readSettings(settingsPath) {
222
- if (fs.existsSync(settingsPath)) {
223
- try {
224
- return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
225
- } catch (e) {
226
- return {};
227
- }
228
- }
229
- return {};
230
- }
231
-
232
- /**
233
- * Write settings.json with proper formatting
234
- */
235
- function writeSettings(settingsPath, settings) {
236
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
237
- }
238
-
239
- // Cache for attribution settings (populated once per runtime during install)
240
- const attributionCache = new Map();
241
-
242
- /**
243
- * Get commit attribution setting for a runtime
244
- * @param {string} runtime - 'claude', 'opencode', 'gemini', or 'codex'
245
- * @returns {null|undefined|string} null = remove, undefined = keep default, string = custom
246
- */
247
- function getCommitAttribution(runtime) {
248
- // Return cached value if available
249
- if (attributionCache.has(runtime)) {
250
- return attributionCache.get(runtime);
251
- }
252
-
253
- let result;
254
-
255
- if (runtime === 'opencode') {
256
- const config = readSettings(path.join(getGlobalDir('opencode', null), 'opencode.json'));
257
- result = config.disable_ai_attribution === true ? null : undefined;
258
- } else if (runtime === 'gemini') {
259
- // Gemini: check gemini settings.json for attribution config
260
- const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
261
- if (!settings.attribution || settings.attribution.commit === undefined) {
262
- result = undefined;
263
- } else if (settings.attribution.commit === '') {
264
- result = null;
265
- } else {
266
- result = settings.attribution.commit;
267
- }
268
- } else if (runtime === 'claude') {
269
- // Claude Code
270
- const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
271
- if (!settings.attribution || settings.attribution.commit === undefined) {
272
- result = undefined;
273
- } else if (settings.attribution.commit === '') {
274
- result = null;
275
- } else {
276
- result = settings.attribution.commit;
277
- }
278
- } else {
279
- // Codex currently has no attribution setting equivalent
280
- result = undefined;
281
- }
282
-
283
- // Cache and return
284
- attributionCache.set(runtime, result);
285
- return result;
286
- }
287
-
288
- /**
289
- * Process Co-Authored-By lines based on attribution setting
290
- * @param {string} content - File content to process
291
- * @param {null|undefined|string} attribution - null=remove, undefined=keep, string=replace
292
- * @returns {string} Processed content
293
- */
294
- function processAttribution(content, attribution) {
295
- if (attribution === null) {
296
- // Remove Co-Authored-By lines and the preceding blank line
297
- return content.replace(/(\r?\n){2}Co-Authored-By:.*$/gim, '');
298
- }
299
- if (attribution === undefined) {
300
- return content;
301
- }
302
- // Replace with custom attribution (escape $ to prevent backreference injection)
303
- const safeAttribution = attribution.replace(/\$/g, '$$$$');
304
- return content.replace(/Co-Authored-By:.*$/gim, `Co-Authored-By: ${safeAttribution}`);
305
- }
306
-
307
- /**
308
- * Convert Claude Code frontmatter to opencode format
309
- * - Converts 'allowed-tools:' array to 'permission:' object
310
- * @param {string} content - Markdown file content with YAML frontmatter
311
- * @returns {string} - Content with converted frontmatter
312
- */
313
- // Color name to hex mapping for opencode compatibility
314
- const colorNameToHex = {
315
- cyan: '#00FFFF',
316
- red: '#FF0000',
317
- green: '#00FF00',
318
- blue: '#0000FF',
319
- yellow: '#FFFF00',
320
- magenta: '#FF00FF',
321
- orange: '#FFA500',
322
- purple: '#800080',
323
- pink: '#FFC0CB',
324
- white: '#FFFFFF',
325
- black: '#000000',
326
- gray: '#808080',
327
- grey: '#808080',
328
- };
59
+ c.cyan(' ██████╗ ███████╗██████╗\n') +
60
+ c.cyan(' ██╔════╝ ██╔════╝██╔══██╗\n') +
61
+ c.cyan(' ██║ ███╗███████╗██║ ██║\n') +
62
+ c.cyan(' ██║ ██║╚════██║██║ ██║\n') +
63
+ c.cyan(' ╚██████╔╝███████║██████╔╝\n') +
64
+ c.cyan(' ╚═════╝ ╚══════╝╚═════╝') + '\n' +
65
+ '\n' +
66
+ ' Get Shit Done ' + c.dim('v' + pkg.version) + '\n' +
67
+ ' Context engineering for GitHub Copilot & OpenCode.\n';
329
68
 
330
- // Tool name mapping from Claude Code to OpenCode
331
- // OpenCode uses lowercase tool names; special mappings for renamed tools
332
- const claudeToOpencodeTools = {
333
- AskUserQuestion: 'question',
334
- SlashCommand: 'skill',
335
- TodoWrite: 'todowrite',
336
- WebFetch: 'webfetch',
337
- WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
338
- };
69
+ // ─── Args ────────────────────────────────────────────────────────────────────
339
70
 
340
- // Tool name mapping from Claude Code to Gemini CLI
341
- // Gemini CLI uses snake_case built-in tool names
342
- const claudeToGeminiTools = {
343
- Read: 'read_file',
344
- Write: 'write_file',
345
- Edit: 'replace',
346
- Bash: 'run_shell_command',
347
- Glob: 'glob',
348
- Grep: 'search_file_content',
349
- WebSearch: 'google_web_search',
350
- WebFetch: 'web_fetch',
351
- TodoWrite: 'write_todos',
352
- AskUserQuestion: 'ask_user',
71
+ const args = process.argv.slice(2);
72
+ const hasCopilot = args.includes('--copilot');
73
+ const hasOpencode = args.includes('--opencode');
74
+ const hasBoth = args.includes('--both') || args.includes('--all');
75
+ const force = args.includes('--force') || args.includes('-f');
76
+ const hasUninstall= args.includes('--uninstall') || args.includes('-u');
77
+ const hasHelp = args.includes('--help') || args.includes('-h');
78
+
79
+ // Target directory — everything installs into the project root
80
+ const targetArg = args.find(a => !a.startsWith('-'));
81
+ const TARGET = targetArg ? path.resolve(targetArg) : process.cwd();
82
+
83
+ // ─── Source paths (inside the npm package) ───────────────────────────────────
84
+
85
+ const PKG_ROOT = path.resolve(__dirname, '..');
86
+
87
+ const SRC = {
88
+ // Copilot sources (live at repo root, same files VS Code scans)
89
+ copilotInstructions: path.join(PKG_ROOT, '.github', 'copilot-instructions.md'),
90
+ copilotPrompts: path.join(PKG_ROOT, '.github', 'prompts'),
91
+ copilotInstructDir: path.join(PKG_ROOT, '.github', 'instructions'),
92
+ copilotAgents: path.join(PKG_ROOT, '.github', 'agents'),
93
+
94
+ // OpenCode sources (hand-crafted templates)
95
+ opencodeAgentsMd: path.join(PKG_ROOT, 'get-shit-done', 'templates', 'AGENTS.md'),
96
+ opencodeJson: path.join(PKG_ROOT, 'get-shit-done', 'templates', 'opencode.json'),
97
+ opencodeAgents: path.join(PKG_ROOT, 'get-shit-done', 'templates', 'opencode', 'agents'),
98
+ opencodeCommands: path.join(PKG_ROOT, 'get-shit-done', 'templates', 'opencode', 'commands'),
99
+
100
+ // Shared
101
+ planning: path.join(PKG_ROOT, '.planning'),
353
102
  };
354
103
 
355
- /**
356
- * Convert a Claude Code tool name to OpenCode format
357
- * - Applies special mappings (AskUserQuestion -> question, etc.)
358
- * - Converts to lowercase (except MCP tools which keep their format)
359
- */
360
- function convertToolName(claudeTool) {
361
- // Check for special mapping first
362
- if (claudeToOpencodeTools[claudeTool]) {
363
- return claudeToOpencodeTools[claudeTool];
364
- }
365
- // MCP tools (mcp__*) keep their format
366
- if (claudeTool.startsWith('mcp__')) {
367
- return claudeTool;
368
- }
369
- // Default: convert to lowercase
370
- return claudeTool.toLowerCase();
371
- }
372
-
373
- /**
374
- * Convert a Claude Code tool name to Gemini CLI format
375
- * - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.)
376
- * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini
377
- * - Filters out Task — agents are auto-registered as tools in Gemini
378
- * @returns {string|null} Gemini tool name, or null if tool should be excluded
379
- */
380
- function convertGeminiToolName(claudeTool) {
381
- // MCP tools: exclude — auto-discovered from mcpServers config at runtime
382
- if (claudeTool.startsWith('mcp__')) {
383
- return null;
384
- }
385
- // Task: exclude — agents are auto-registered as callable tools
386
- if (claudeTool === 'Task') {
387
- return null;
388
- }
389
- // Check for explicit mapping
390
- if (claudeToGeminiTools[claudeTool]) {
391
- return claudeToGeminiTools[claudeTool];
392
- }
393
- // Default: lowercase
394
- return claudeTool.toLowerCase();
395
- }
396
-
397
- function toSingleLine(value) {
398
- return value.replace(/\s+/g, ' ').trim();
399
- }
400
-
401
- function yamlQuote(value) {
402
- return JSON.stringify(value);
403
- }
404
-
405
- function extractFrontmatterAndBody(content) {
406
- if (!content.startsWith('---')) {
407
- return { frontmatter: null, body: content };
408
- }
409
-
410
- const endIndex = content.indexOf('---', 3);
411
- if (endIndex === -1) {
412
- return { frontmatter: null, body: content };
413
- }
414
-
415
- return {
416
- frontmatter: content.substring(3, endIndex).trim(),
417
- body: content.substring(endIndex + 3),
418
- };
419
- }
420
-
421
- function extractFrontmatterField(frontmatter, fieldName) {
422
- const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, 'm');
423
- const match = frontmatter.match(regex);
424
- if (!match) return null;
425
- return match[1].trim().replace(/^['"]|['"]$/g, '');
426
- }
427
-
428
- function convertSlashCommandsToCodexSkillMentions(content) {
429
- let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
430
- return `$gsd-${String(commandName).toLowerCase()}`;
431
- });
432
- converted = converted.replace(/\/gsd-help\b/g, '$gsd-help');
433
- return converted;
434
- }
435
-
436
- function convertClaudeToCodexMarkdown(content) {
437
- let converted = convertSlashCommandsToCodexSkillMentions(content);
438
- converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
439
- return converted;
440
- }
441
-
442
- function getCodexSkillAdapterHeader(skillName) {
443
- const invocation = `$${skillName}`;
444
- return `<codex_skill_adapter>
445
- Codex skills-first mode:
446
- - This skill is invoked by mentioning \`${invocation}\`.
447
- - Treat all user text after \`${invocation}\` as \`{{GSD_ARGS}}\`.
448
- - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
449
-
450
- Legacy orchestration compatibility:
451
- - Any \`Task(...)\` pattern in referenced workflow docs is legacy syntax.
452
- - Implement equivalent behavior with Codex collaboration tools: \`spawn_agent\`, \`wait\`, \`send_input\`, and \`close_agent\`.
453
- - Treat legacy \`subagent_type\` names as role hints in the spawned message.
454
- </codex_skill_adapter>`;
455
- }
456
-
457
- function convertClaudeCommandToCodexSkill(content, skillName) {
458
- const converted = convertClaudeToCodexMarkdown(content);
459
- const { frontmatter, body } = extractFrontmatterAndBody(converted);
460
- let description = `Run GSD workflow ${skillName}.`;
461
- if (frontmatter) {
462
- const maybeDescription = extractFrontmatterField(frontmatter, 'description');
463
- if (maybeDescription) {
464
- description = maybeDescription;
465
- }
466
- }
467
- description = toSingleLine(description);
468
- const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
469
- const adapter = getCodexSkillAdapterHeader(skillName);
470
-
471
- return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
472
- }
473
-
474
- /**
475
- * Strip HTML <sub> tags for Gemini CLI output
476
- * Terminals don't support subscript — Gemini renders these as raw HTML.
477
- * Converts <sub>text</sub> to italic *(text)* for readable terminal output.
478
- */
479
- function stripSubTags(content) {
480
- return content.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
481
- }
482
-
483
- /**
484
- * Convert Claude Code agent frontmatter to Gemini CLI format
485
- * Gemini agents use .md files with YAML frontmatter, same as Claude,
486
- * but with different field names and formats:
487
- * - tools: must be a YAML array (not comma-separated string)
488
- * - tool names: must use Gemini built-in names (read_file, not Read)
489
- * - color: must be removed (causes validation error)
490
- * - mcp__* tools: must be excluded (auto-discovered at runtime)
491
- */
492
- function convertClaudeToGeminiAgent(content) {
493
- if (!content.startsWith('---')) return content;
494
-
495
- const endIndex = content.indexOf('---', 3);
496
- if (endIndex === -1) return content;
497
-
498
- const frontmatter = content.substring(3, endIndex).trim();
499
- const body = content.substring(endIndex + 3);
500
-
501
- const lines = frontmatter.split('\n');
502
- const newLines = [];
503
- let inAllowedTools = false;
504
- const tools = [];
505
-
506
- for (const line of lines) {
507
- const trimmed = line.trim();
508
-
509
- // Convert allowed-tools YAML array to tools list
510
- if (trimmed.startsWith('allowed-tools:')) {
511
- inAllowedTools = true;
512
- continue;
513
- }
514
-
515
- // Handle inline tools: field (comma-separated string)
516
- if (trimmed.startsWith('tools:')) {
517
- const toolsValue = trimmed.substring(6).trim();
518
- if (toolsValue) {
519
- const parsed = toolsValue.split(',').map(t => t.trim()).filter(t => t);
520
- for (const t of parsed) {
521
- const mapped = convertGeminiToolName(t);
522
- if (mapped) tools.push(mapped);
523
- }
524
- } else {
525
- // tools: with no value means YAML array follows
526
- inAllowedTools = true;
527
- }
528
- continue;
529
- }
530
-
531
- // Strip color field (not supported by Gemini CLI, causes validation error)
532
- if (trimmed.startsWith('color:')) continue;
533
-
534
- // Collect allowed-tools/tools array items
535
- if (inAllowedTools) {
536
- if (trimmed.startsWith('- ')) {
537
- const mapped = convertGeminiToolName(trimmed.substring(2).trim());
538
- if (mapped) tools.push(mapped);
539
- continue;
540
- } else if (trimmed && !trimmed.startsWith('-')) {
541
- inAllowedTools = false;
542
- }
543
- }
544
-
545
- if (!inAllowedTools) {
546
- newLines.push(line);
547
- }
548
- }
549
-
550
- // Add tools as YAML array (Gemini requires array format)
551
- if (tools.length > 0) {
552
- newLines.push('tools:');
553
- for (const tool of tools) {
554
- newLines.push(` - ${tool}`);
555
- }
556
- }
557
-
558
- const newFrontmatter = newLines.join('\n').trim();
559
-
560
- // Escape ${VAR} patterns in agent body for Gemini CLI compatibility.
561
- // Gemini's templateString() treats all ${word} patterns as template variables
562
- // and throws "Template validation failed: Missing required input parameters"
563
- // when they can't be resolved. GSD agents use ${PHASE}, ${PLAN}, etc. as
564
- // shell variables in bash code blocks — convert to $VAR (no braces) which
565
- // is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex.
566
- const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
567
-
568
- return `---\n${newFrontmatter}\n---${stripSubTags(escapedBody)}`;
569
- }
570
-
571
- function convertClaudeToOpencodeFrontmatter(content) {
572
- // Replace tool name references in content (applies to all files)
573
- let convertedContent = content;
574
- convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
575
- convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
576
- convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
577
- // Replace /gsd:command with /gsd-command for opencode (flat command structure)
578
- convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-');
579
- // Replace ~/.claude with ~/.config/opencode (OpenCode's correct config location)
580
- convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
581
- // Replace general-purpose subagent type with OpenCode's equivalent "general"
582
- convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
583
-
584
- // Check if content has frontmatter
585
- if (!convertedContent.startsWith('---')) {
586
- return convertedContent;
587
- }
588
-
589
- // Find the end of frontmatter
590
- const endIndex = convertedContent.indexOf('---', 3);
591
- if (endIndex === -1) {
592
- return convertedContent;
593
- }
594
-
595
- const frontmatter = convertedContent.substring(3, endIndex).trim();
596
- const body = convertedContent.substring(endIndex + 3);
597
-
598
- // Parse frontmatter line by line (simple YAML parsing)
599
- const lines = frontmatter.split('\n');
600
- const newLines = [];
601
- let inAllowedTools = false;
602
- const allowedTools = [];
603
-
604
- for (const line of lines) {
605
- const trimmed = line.trim();
606
-
607
- // Detect start of allowed-tools array
608
- if (trimmed.startsWith('allowed-tools:')) {
609
- inAllowedTools = true;
610
- continue;
611
- }
612
-
613
- // Detect inline tools: field (comma-separated string)
614
- if (trimmed.startsWith('tools:')) {
615
- const toolsValue = trimmed.substring(6).trim();
616
- if (toolsValue) {
617
- // Parse comma-separated tools
618
- const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
619
- allowedTools.push(...tools);
620
- }
621
- continue;
622
- }
623
-
624
- // Remove name: field - opencode uses filename for command name
625
- if (trimmed.startsWith('name:')) {
626
- continue;
627
- }
628
-
629
- // Convert color names to hex for opencode
630
- if (trimmed.startsWith('color:')) {
631
- const colorValue = trimmed.substring(6).trim().toLowerCase();
632
- const hexColor = colorNameToHex[colorValue];
633
- if (hexColor) {
634
- newLines.push(`color: "${hexColor}"`);
635
- } else if (colorValue.startsWith('#')) {
636
- // Validate hex color format (#RGB or #RRGGBB)
637
- if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) {
638
- // Already hex and valid, keep as is
639
- newLines.push(line);
640
- }
641
- // Skip invalid hex colors
642
- }
643
- // Skip unknown color names
644
- continue;
645
- }
646
-
647
- // Collect allowed-tools items
648
- if (inAllowedTools) {
649
- if (trimmed.startsWith('- ')) {
650
- allowedTools.push(trimmed.substring(2).trim());
651
- continue;
652
- } else if (trimmed && !trimmed.startsWith('-')) {
653
- // End of array, new field started
654
- inAllowedTools = false;
655
- }
656
- }
657
-
658
- // Keep other fields (including name: which opencode ignores)
659
- if (!inAllowedTools) {
660
- newLines.push(line);
661
- }
662
- }
663
-
664
- // Add tools object if we had allowed-tools or tools
665
- if (allowedTools.length > 0) {
666
- newLines.push('tools:');
667
- for (const tool of allowedTools) {
668
- newLines.push(` ${convertToolName(tool)}: true`);
669
- }
670
- }
104
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
671
105
 
672
- // Rebuild frontmatter (body already has tool names converted)
673
- const newFrontmatter = newLines.join('\n').trim();
674
- return `---\n${newFrontmatter}\n---${body}`;
106
+ function ensureDir(dir) {
107
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
675
108
  }
676
109
 
677
- /**
678
- * Convert Claude Code markdown command to Gemini TOML format
679
- * @param {string} content - Markdown file content with YAML frontmatter
680
- * @returns {string} - TOML content
681
- */
682
- function convertClaudeToGeminiToml(content) {
683
- // Check if content has frontmatter
684
- if (!content.startsWith('---')) {
685
- return `prompt = ${JSON.stringify(content)}\n`;
686
- }
687
-
688
- const endIndex = content.indexOf('---', 3);
689
- if (endIndex === -1) {
690
- return `prompt = ${JSON.stringify(content)}\n`;
691
- }
692
-
693
- const frontmatter = content.substring(3, endIndex).trim();
694
- const body = content.substring(endIndex + 3).trim();
695
-
696
- // Extract description from frontmatter
697
- let description = '';
698
- const lines = frontmatter.split('\n');
699
- for (const line of lines) {
700
- const trimmed = line.trim();
701
- if (trimmed.startsWith('description:')) {
702
- description = trimmed.substring(12).trim();
703
- break;
704
- }
705
- }
706
-
707
- // Construct TOML
708
- let toml = '';
709
- if (description) {
710
- toml += `description = ${JSON.stringify(description)}\n`;
711
- }
712
-
713
- toml += `prompt = ${JSON.stringify(body)}\n`;
714
-
715
- return toml;
110
+ /** Copy a single file. Returns 'copied' | 'skipped' | 'overwritten'. */
111
+ function copyFile(src, dest) {
112
+ ensureDir(path.dirname(dest));
113
+ const exists = fs.existsSync(dest);
114
+ if (exists && !force) return 'skipped';
115
+ fs.copyFileSync(src, dest);
116
+ return exists ? 'overwritten' : 'copied';
716
117
  }
717
118
 
718
- /**
719
- * Copy commands to a flat structure for OpenCode
720
- * OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
721
- * Source structure: commands/gsd/help.md
722
- *
723
- * @param {string} srcDir - Source directory (e.g., commands/gsd/)
724
- * @param {string} destDir - Destination directory (e.g., command/)
725
- * @param {string} prefix - Prefix for filenames (e.g., 'gsd')
726
- * @param {string} pathPrefix - Path prefix for file references
727
- * @param {string} runtime - Target runtime ('claude' or 'opencode')
728
- */
729
- function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
730
- if (!fs.existsSync(srcDir)) {
731
- return;
732
- }
733
-
734
- // Remove old gsd-*.md files before copying new ones
735
- if (fs.existsSync(destDir)) {
736
- for (const file of fs.readdirSync(destDir)) {
737
- if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
738
- fs.unlinkSync(path.join(destDir, file));
739
- }
740
- }
741
- } else {
742
- fs.mkdirSync(destDir, { recursive: true });
743
- }
744
-
745
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
746
-
747
- for (const entry of entries) {
748
- const srcPath = path.join(srcDir, entry.name);
749
-
119
+ /** Recursively copy a directory tree. */
120
+ function copyDir(src, dest, results = []) {
121
+ ensureDir(dest);
122
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
123
+ const s = path.join(src, entry.name);
124
+ const d = path.join(dest, entry.name);
750
125
  if (entry.isDirectory()) {
751
- // Recurse into subdirectories, adding to prefix
752
- // e.g., commands/gsd/debug/start.md -> command/gsd-debug-start.md
753
- copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
754
- } else if (entry.name.endsWith('.md')) {
755
- // Flatten: help.md -> gsd-help.md
756
- const baseName = entry.name.replace('.md', '');
757
- const destName = `${prefix}-${baseName}.md`;
758
- const destPath = path.join(destDir, destName);
759
-
760
- let content = fs.readFileSync(srcPath, 'utf8');
761
- const globalClaudeRegex = /~\/\.claude\//g;
762
- const localClaudeRegex = /\.\/\.claude\//g;
763
- const opencodeDirRegex = /~\/\.opencode\//g;
764
- content = content.replace(globalClaudeRegex, pathPrefix);
765
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
766
- content = content.replace(opencodeDirRegex, pathPrefix);
767
- content = processAttribution(content, getCommitAttribution(runtime));
768
- content = convertClaudeToOpencodeFrontmatter(content);
769
-
770
- fs.writeFileSync(destPath, content);
126
+ copyDir(s, d, results);
127
+ } else {
128
+ results.push({ dest: d, status: copyFile(s, d) });
771
129
  }
772
130
  }
131
+ return results;
773
132
  }
774
133
 
775
- function listCodexSkillNames(skillsDir, prefix = 'gsd-') {
776
- if (!fs.existsSync(skillsDir)) return [];
777
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
778
- return entries
779
- .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
780
- .filter(entry => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
781
- .map(entry => entry.name)
782
- .sort();
783
- }
784
-
785
- function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
786
- if (!fs.existsSync(srcDir)) {
787
- return;
788
- }
789
-
790
- fs.mkdirSync(skillsDir, { recursive: true });
791
-
792
- // Remove previous GSD Codex skills to avoid stale command skills.
793
- const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
794
- for (const entry of existing) {
795
- if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
796
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
797
- }
798
- }
799
-
800
- function recurse(currentSrcDir, currentPrefix) {
801
- const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
802
-
803
- for (const entry of entries) {
804
- const srcPath = path.join(currentSrcDir, entry.name);
805
- if (entry.isDirectory()) {
806
- recurse(srcPath, `${currentPrefix}-${entry.name}`);
807
- continue;
808
- }
809
-
810
- if (!entry.name.endsWith('.md')) {
811
- continue;
812
- }
813
-
814
- const baseName = entry.name.replace('.md', '');
815
- const skillName = `${currentPrefix}-${baseName}`;
816
- const skillDir = path.join(skillsDir, skillName);
817
- fs.mkdirSync(skillDir, { recursive: true });
818
-
819
- let content = fs.readFileSync(srcPath, 'utf8');
820
- const globalClaudeRegex = /~\/\.claude\//g;
821
- const localClaudeRegex = /\.\/\.claude\//g;
822
- const codexDirRegex = /~\/\.codex\//g;
823
- content = content.replace(globalClaudeRegex, pathPrefix);
824
- content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
825
- content = content.replace(codexDirRegex, pathPrefix);
826
- content = processAttribution(content, getCommitAttribution(runtime));
827
- content = convertClaudeCommandToCodexSkill(content, skillName);
828
-
829
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
830
- }
134
+ /** Remove a directory tree if it exists. */
135
+ function removeDir(dir) {
136
+ if (fs.existsSync(dir)) {
137
+ fs.rmSync(dir, { recursive: true, force: true });
138
+ return true;
831
139
  }
832
-
833
- recurse(srcDir, prefix);
140
+ return false;
834
141
  }
835
142
 
836
- /**
837
- * Recursively copy directory, replacing paths in .md files
838
- * Deletes existing destDir first to remove orphaned files from previous versions
839
- * @param {string} srcDir - Source directory
840
- * @param {string} destDir - Destination directory
841
- * @param {string} pathPrefix - Path prefix for file references
842
- * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
843
- */
844
- function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false) {
845
- const isOpencode = runtime === 'opencode';
846
- const isCodex = runtime === 'codex';
847
- const dirName = getDirName(runtime);
848
-
849
- // Clean install: remove existing destination to prevent orphaned files
850
- if (fs.existsSync(destDir)) {
851
- fs.rmSync(destDir, { recursive: true });
852
- }
853
- fs.mkdirSync(destDir, { recursive: true });
854
-
855
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
856
-
857
- for (const entry of entries) {
858
- const srcPath = path.join(srcDir, entry.name);
859
- const destPath = path.join(destDir, entry.name);
860
-
861
- if (entry.isDirectory()) {
862
- copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand);
863
- } else if (entry.name.endsWith('.md')) {
864
- // Replace ~/.claude/ and ./.claude/ with runtime-appropriate paths
865
- let content = fs.readFileSync(srcPath, 'utf8');
866
- const globalClaudeRegex = /~\/\.claude\//g;
867
- const localClaudeRegex = /\.\/\.claude\//g;
868
- content = content.replace(globalClaudeRegex, pathPrefix);
869
- content = content.replace(localClaudeRegex, `./${dirName}/`);
870
- content = processAttribution(content, getCommitAttribution(runtime));
871
-
872
- // Convert frontmatter for opencode compatibility
873
- if (isOpencode) {
874
- content = convertClaudeToOpencodeFrontmatter(content);
875
- fs.writeFileSync(destPath, content);
876
- } else if (runtime === 'gemini') {
877
- if (isCommand) {
878
- // Convert to TOML for Gemini (strip <sub> tags — terminals can't render subscript)
879
- content = stripSubTags(content);
880
- const tomlContent = convertClaudeToGeminiToml(content);
881
- // Replace extension with .toml
882
- const tomlPath = destPath.replace(/\.md$/, '.toml');
883
- fs.writeFileSync(tomlPath, tomlContent);
884
- } else {
885
- fs.writeFileSync(destPath, content);
886
- }
887
- } else if (isCodex) {
888
- content = convertClaudeToCodexMarkdown(content);
889
- fs.writeFileSync(destPath, content);
890
- } else {
891
- fs.writeFileSync(destPath, content);
892
- }
893
- } else {
894
- fs.copyFileSync(srcPath, destPath);
895
- }
143
+ /** Remove a single file if it exists. */
144
+ function removeFile(file) {
145
+ if (fs.existsSync(file)) {
146
+ fs.unlinkSync(file);
147
+ return true;
896
148
  }
149
+ return false;
897
150
  }
898
151
 
899
- /**
900
- * Clean up orphaned files from previous GSD versions
901
- */
902
- function cleanupOrphanedFiles(configDir) {
903
- const orphanedFiles = [
904
- 'hooks/gsd-notify.sh', // Removed in v1.6.x
905
- 'hooks/statusline.js', // Renamed to gsd-statusline.js in v1.9.0
906
- ];
907
-
908
- for (const relPath of orphanedFiles) {
909
- const fullPath = path.join(configDir, relPath);
910
- if (fs.existsSync(fullPath)) {
911
- fs.unlinkSync(fullPath);
912
- console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
913
- }
914
- }
152
+ function rel(p) {
153
+ return path.relative(TARGET, p);
915
154
  }
916
155
 
917
- /**
918
- * Clean up orphaned hook registrations from settings.json
919
- */
920
- function cleanupOrphanedHooks(settings) {
921
- const orphanedHookPatterns = [
922
- 'gsd-notify.sh', // Removed in v1.6.x
923
- 'hooks/statusline.js', // Renamed to gsd-statusline.js in v1.9.0
924
- 'gsd-intel-index.js', // Removed in v1.9.2
925
- 'gsd-intel-session.js', // Removed in v1.9.2
926
- 'gsd-intel-prune.js', // Removed in v1.9.2
927
- ];
928
-
929
- let cleanedHooks = false;
930
-
931
- // Check all hook event types (Stop, SessionStart, etc.)
932
- if (settings.hooks) {
933
- for (const eventType of Object.keys(settings.hooks)) {
934
- const hookEntries = settings.hooks[eventType];
935
- if (Array.isArray(hookEntries)) {
936
- // Filter out entries that contain orphaned hooks
937
- const filtered = hookEntries.filter(entry => {
938
- if (entry.hooks && Array.isArray(entry.hooks)) {
939
- // Check if any hook in this entry matches orphaned patterns
940
- const hasOrphaned = entry.hooks.some(h =>
941
- h.command && orphanedHookPatterns.some(pattern => h.command.includes(pattern))
942
- );
943
- if (hasOrphaned) {
944
- cleanedHooks = true;
945
- return false; // Remove this entry
946
- }
947
- }
948
- return true; // Keep this entry
949
- });
950
- settings.hooks[eventType] = filtered;
951
- }
952
- }
953
- }
156
+ // ─── Pretty print ────────────────────────────────────────────────────────────
954
157
 
955
- if (cleanedHooks) {
956
- console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
158
+ function printResults(results) {
159
+ let copied = 0, skipped = 0, overwritten = 0;
160
+ for (const { dest, status } of results) {
161
+ if (status === 'copied') { copied++; console.log(` ${c.green('+')} ${rel(dest)}`); }
162
+ if (status === 'overwritten') { overwritten++; console.log(` ${c.yellow('~')} ${rel(dest)}`); }
163
+ if (status === 'skipped') { skipped++; console.log(` ${c.dim('·')} ${rel(dest)} ${c.dim('(skipped)')}`); }
957
164
  }
958
-
959
- // Fix #330: Update statusLine if it points to old statusline.js path
960
- if (settings.statusLine && settings.statusLine.command &&
961
- settings.statusLine.command.includes('statusline.js') &&
962
- !settings.statusLine.command.includes('gsd-statusline.js')) {
963
- // Replace old path with new path
964
- settings.statusLine.command = settings.statusLine.command.replace(
965
- /statusline\.js/,
966
- 'gsd-statusline.js'
967
- );
968
- console.log(` ${green}✓${reset} Updated statusline path (statusline.js → gsd-statusline.js)`);
969
- }
970
-
971
- return settings;
165
+ return { copied, skipped, overwritten };
972
166
  }
973
167
 
168
+ // ─── JSON merge helper (for opencode.json) ───────────────────────────────────
169
+
974
170
  /**
975
- * Uninstall GSD from the specified directory for a specific runtime
976
- * Removes only GSD-specific files/directories, preserves user content
977
- * @param {boolean} isGlobal - Whether to uninstall from global or local
978
- * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
171
+ * Merge template opencode.json into existing project config.
172
+ * - Merges `instructions` array (appends missing entries)
173
+ * - Preserves all existing user config
979
174
  */
980
- function uninstall(isGlobal, runtime = 'claude') {
981
- const isOpencode = runtime === 'opencode';
982
- const isCodex = runtime === 'codex';
983
- const dirName = getDirName(runtime);
984
-
985
- // Get the target directory based on runtime and install type
986
- const targetDir = isGlobal
987
- ? getGlobalDir(runtime, explicitConfigDir)
988
- : path.join(process.cwd(), dirName);
989
-
990
- const locationLabel = isGlobal
991
- ? targetDir.replace(os.homedir(), '~')
992
- : targetDir.replace(process.cwd(), '.');
993
-
994
- let runtimeLabel = 'Claude Code';
995
- if (runtime === 'opencode') runtimeLabel = 'OpenCode';
996
- if (runtime === 'gemini') runtimeLabel = 'Gemini';
997
- if (runtime === 'codex') runtimeLabel = 'Codex';
998
-
999
- console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
1000
-
1001
- // Check if target directory exists
1002
- if (!fs.existsSync(targetDir)) {
1003
- console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`);
1004
- console.log(` Nothing to uninstall.\n`);
1005
- return;
1006
- }
1007
-
1008
- let removedCount = 0;
1009
-
1010
- // 1. Remove GSD commands/skills
1011
- if (isOpencode) {
1012
- // OpenCode: remove command/gsd-*.md files
1013
- const commandDir = path.join(targetDir, 'command');
1014
- if (fs.existsSync(commandDir)) {
1015
- const files = fs.readdirSync(commandDir);
1016
- for (const file of files) {
1017
- if (file.startsWith('gsd-') && file.endsWith('.md')) {
1018
- fs.unlinkSync(path.join(commandDir, file));
1019
- removedCount++;
1020
- }
1021
- }
1022
- console.log(` ${green}✓${reset} Removed GSD commands from command/`);
1023
- }
1024
- } else if (isCodex) {
1025
- // Codex: remove skills/gsd-*/SKILL.md skill directories
1026
- const skillsDir = path.join(targetDir, 'skills');
1027
- if (fs.existsSync(skillsDir)) {
1028
- let skillCount = 0;
1029
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
1030
- for (const entry of entries) {
1031
- if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
1032
- fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
1033
- skillCount++;
1034
- }
1035
- }
1036
- if (skillCount > 0) {
1037
- removedCount++;
1038
- console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`);
1039
- }
1040
- }
1041
- } else {
1042
- // Claude Code & Gemini: remove commands/gsd/ directory
1043
- const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
1044
- if (fs.existsSync(gsdCommandsDir)) {
1045
- fs.rmSync(gsdCommandsDir, { recursive: true });
1046
- removedCount++;
1047
- console.log(` ${green}✓${reset} Removed commands/gsd/`);
1048
- }
1049
- }
1050
-
1051
- // 2. Remove get-shit-done directory
1052
- const gsdDir = path.join(targetDir, 'get-shit-done');
1053
- if (fs.existsSync(gsdDir)) {
1054
- fs.rmSync(gsdDir, { recursive: true });
1055
- removedCount++;
1056
- console.log(` ${green}✓${reset} Removed get-shit-done/`);
1057
- }
1058
-
1059
- // 3. Remove GSD agents (gsd-*.md files only)
1060
- const agentsDir = path.join(targetDir, 'agents');
1061
- if (fs.existsSync(agentsDir)) {
1062
- const files = fs.readdirSync(agentsDir);
1063
- let agentCount = 0;
1064
- for (const file of files) {
1065
- if (file.startsWith('gsd-') && file.endsWith('.md')) {
1066
- fs.unlinkSync(path.join(agentsDir, file));
1067
- agentCount++;
1068
- }
1069
- }
1070
- if (agentCount > 0) {
1071
- removedCount++;
1072
- console.log(` ${green}✓${reset} Removed ${agentCount} GSD agents`);
1073
- }
1074
- }
1075
-
1076
- // 4. Remove GSD hooks
1077
- const hooksDir = path.join(targetDir, 'hooks');
1078
- if (fs.existsSync(hooksDir)) {
1079
- const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh', 'gsd-context-monitor.js'];
1080
- let hookCount = 0;
1081
- for (const hook of gsdHooks) {
1082
- const hookPath = path.join(hooksDir, hook);
1083
- if (fs.existsSync(hookPath)) {
1084
- fs.unlinkSync(hookPath);
1085
- hookCount++;
1086
- }
1087
- }
1088
- if (hookCount > 0) {
1089
- removedCount++;
1090
- console.log(` ${green}✓${reset} Removed ${hookCount} GSD hooks`);
1091
- }
175
+ function mergeOpencodeJson(templatePath, destPath) {
176
+ let template = {};
177
+ try {
178
+ template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
179
+ } catch (e) {
180
+ console.error(c.red(` Error reading template: ${e.message}`));
181
+ return 'skipped';
1092
182
  }
1093
183
 
1094
- // 5. Remove GSD package.json (CommonJS mode marker)
1095
- const pkgJsonPath = path.join(targetDir, 'package.json');
1096
- if (fs.existsSync(pkgJsonPath)) {
184
+ let existing = {};
185
+ const exists = fs.existsSync(destPath);
186
+ if (exists) {
1097
187
  try {
1098
- const content = fs.readFileSync(pkgJsonPath, 'utf8').trim();
1099
- // Only remove if it's our minimal CommonJS marker
1100
- if (content === '{"type":"commonjs"}') {
1101
- fs.unlinkSync(pkgJsonPath);
1102
- removedCount++;
1103
- console.log(` ${green}✓${reset} Removed GSD package.json`);
1104
- }
188
+ existing = JSON.parse(fs.readFileSync(destPath, 'utf8'));
1105
189
  } catch (e) {
1106
- // Ignore read errors
1107
- }
1108
- }
1109
-
1110
- // 6. Clean up settings.json (remove GSD hooks and statusline)
1111
- const settingsPath = path.join(targetDir, 'settings.json');
1112
- if (fs.existsSync(settingsPath)) {
1113
- let settings = readSettings(settingsPath);
1114
- let settingsModified = false;
1115
-
1116
- // Remove GSD statusline if it references our hook
1117
- if (settings.statusLine && settings.statusLine.command &&
1118
- settings.statusLine.command.includes('gsd-statusline')) {
1119
- delete settings.statusLine;
1120
- settingsModified = true;
1121
- console.log(` ${green}✓${reset} Removed GSD statusline from settings`);
1122
- }
1123
-
1124
- // Remove GSD hooks from SessionStart
1125
- if (settings.hooks && settings.hooks.SessionStart) {
1126
- const before = settings.hooks.SessionStart.length;
1127
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
1128
- if (entry.hooks && Array.isArray(entry.hooks)) {
1129
- // Filter out GSD hooks
1130
- const hasGsdHook = entry.hooks.some(h =>
1131
- h.command && (h.command.includes('gsd-check-update') || h.command.includes('gsd-statusline'))
1132
- );
1133
- return !hasGsdHook;
1134
- }
1135
- return true;
1136
- });
1137
- if (settings.hooks.SessionStart.length < before) {
1138
- settingsModified = true;
1139
- console.log(` ${green}✓${reset} Removed GSD hooks from settings`);
1140
- }
1141
- // Clean up empty array
1142
- if (settings.hooks.SessionStart.length === 0) {
1143
- delete settings.hooks.SessionStart;
1144
- }
1145
- }
1146
-
1147
- // Remove GSD hooks from PostToolUse
1148
- if (settings.hooks && settings.hooks.PostToolUse) {
1149
- const before = settings.hooks.PostToolUse.length;
1150
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
1151
- if (entry.hooks && Array.isArray(entry.hooks)) {
1152
- const hasGsdHook = entry.hooks.some(h =>
1153
- h.command && h.command.includes('gsd-context-monitor')
1154
- );
1155
- return !hasGsdHook;
1156
- }
1157
- return true;
1158
- });
1159
- if (settings.hooks.PostToolUse.length < before) {
1160
- settingsModified = true;
1161
- console.log(` ${green}✓${reset} Removed context monitor hook from settings`);
190
+ if (!force) {
191
+ console.log(c.yellow(` ⚠ Could not parse existing opencode.json — skipping (use --force to overwrite)`));
192
+ return 'skipped';
1162
193
  }
1163
- if (settings.hooks.PostToolUse.length === 0) {
1164
- delete settings.hooks.PostToolUse;
1165
- }
1166
- }
1167
-
1168
- // Clean up empty hooks object
1169
- if (settings.hooks && Object.keys(settings.hooks).length === 0) {
1170
- delete settings.hooks;
1171
- }
1172
-
1173
- if (settingsModified) {
1174
- writeSettings(settingsPath, settings);
1175
- removedCount++;
194
+ existing = {};
1176
195
  }
1177
196
  }
1178
197
 
1179
- // 6. For OpenCode, clean up permissions from opencode.json
1180
- if (isOpencode) {
1181
- // For local uninstalls, clean up ./.opencode/opencode.json
1182
- // For global uninstalls, clean up ~/.config/opencode/opencode.json
1183
- const opencodeConfigDir = isGlobal
1184
- ? getOpencodeGlobalDir()
1185
- : path.join(process.cwd(), '.opencode');
1186
- const configPath = path.join(opencodeConfigDir, 'opencode.json');
1187
- if (fs.existsSync(configPath)) {
1188
- try {
1189
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1190
- let modified = false;
1191
-
1192
- // Remove GSD permission entries
1193
- if (config.permission) {
1194
- for (const permType of ['read', 'external_directory']) {
1195
- if (config.permission[permType]) {
1196
- const keys = Object.keys(config.permission[permType]);
1197
- for (const key of keys) {
1198
- if (key.includes('get-shit-done')) {
1199
- delete config.permission[permType][key];
1200
- modified = true;
1201
- }
1202
- }
1203
- // Clean up empty objects
1204
- if (Object.keys(config.permission[permType]).length === 0) {
1205
- delete config.permission[permType];
1206
- }
1207
- }
1208
- }
1209
- if (Object.keys(config.permission).length === 0) {
1210
- delete config.permission;
1211
- }
1212
- }
1213
-
1214
- if (modified) {
1215
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
1216
- removedCount++;
1217
- console.log(` ${green}✓${reset} Removed GSD permissions from opencode.json`);
1218
- }
1219
- } catch (e) {
1220
- // Ignore JSON parse errors
1221
- }
198
+ // Merge instructions array
199
+ if (template.instructions && Array.isArray(template.instructions)) {
200
+ if (!existing.instructions || !Array.isArray(existing.instructions)) {
201
+ existing.instructions = [];
1222
202
  }
1223
- }
1224
-
1225
- if (removedCount === 0) {
1226
- console.log(` ${yellow}⚠${reset} No GSD files found to remove.`);
1227
- }
1228
-
1229
- console.log(`
1230
- ${green}Done!${reset} GSD has been uninstalled from ${runtimeLabel}.
1231
- Your other files and settings have been preserved.
1232
- `);
1233
- }
1234
-
1235
- /**
1236
- * Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
1237
- * OpenCode supports JSONC format via jsonc-parser, so users may have comments.
1238
- * This is a lightweight inline parser to avoid adding dependencies.
1239
- */
1240
- function parseJsonc(content) {
1241
- // Strip BOM if present
1242
- if (content.charCodeAt(0) === 0xFEFF) {
1243
- content = content.slice(1);
1244
- }
1245
-
1246
- // Remove single-line and block comments while preserving strings
1247
- let result = '';
1248
- let inString = false;
1249
- let i = 0;
1250
- while (i < content.length) {
1251
- const char = content[i];
1252
- const next = content[i + 1];
1253
-
1254
- if (inString) {
1255
- result += char;
1256
- // Handle escape sequences
1257
- if (char === '\\' && i + 1 < content.length) {
1258
- result += next;
1259
- i += 2;
1260
- continue;
1261
- }
1262
- if (char === '"') {
1263
- inString = false;
1264
- }
1265
- i++;
1266
- } else {
1267
- if (char === '"') {
1268
- inString = true;
1269
- result += char;
1270
- i++;
1271
- } else if (char === '/' && next === '/') {
1272
- // Skip single-line comment until end of line
1273
- while (i < content.length && content[i] !== '\n') {
1274
- i++;
1275
- }
1276
- } else if (char === '/' && next === '*') {
1277
- // Skip block comment
1278
- i += 2;
1279
- while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
1280
- i++;
1281
- }
1282
- i += 2; // Skip closing */
1283
- } else {
1284
- result += char;
1285
- i++;
203
+ for (const instruction of template.instructions) {
204
+ if (!existing.instructions.includes(instruction)) {
205
+ existing.instructions.push(instruction);
1286
206
  }
1287
207
  }
1288
208
  }
1289
209
 
1290
- // Remove trailing commas before } or ]
1291
- result = result.replace(/,(\s*[}\]])/g, '$1');
210
+ // Set default_agent if not already set
211
+ if (template.default_agent && !existing.default_agent) {
212
+ existing.default_agent = template.default_agent;
213
+ }
1292
214
 
1293
- return JSON.parse(result);
215
+ ensureDir(path.dirname(destPath));
216
+ fs.writeFileSync(destPath, JSON.stringify(existing, null, 2) + '\n');
217
+ return exists ? 'overwritten' : 'copied';
1294
218
  }
1295
219
 
1296
- /**
1297
- * Configure OpenCode permissions to allow reading GSD reference docs
1298
- * This prevents permission prompts when GSD accesses the get-shit-done directory
1299
- * @param {boolean} isGlobal - Whether this is a global or local install
1300
- */
1301
- function configureOpencodePermissions(isGlobal = true) {
1302
- // For local installs, use ./.opencode/opencode.json
1303
- // For global installs, use ~/.config/opencode/opencode.json
1304
- const opencodeConfigDir = isGlobal
1305
- ? getOpencodeGlobalDir()
1306
- : path.join(process.cwd(), '.opencode');
1307
- const configPath = path.join(opencodeConfigDir, 'opencode.json');
1308
-
1309
- // Ensure config directory exists
1310
- fs.mkdirSync(opencodeConfigDir, { recursive: true });
1311
-
1312
- // Read existing config or create empty object
1313
- let config = {};
1314
- if (fs.existsSync(configPath)) {
1315
- try {
1316
- const content = fs.readFileSync(configPath, 'utf8');
1317
- config = parseJsonc(content);
1318
- } catch (e) {
1319
- // Cannot parse - DO NOT overwrite user's config
1320
- console.log(` ${yellow}⚠${reset} Could not parse opencode.json - skipping permission config`);
1321
- console.log(` ${dim}Reason: ${e.message}${reset}`);
1322
- console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
1323
- return;
1324
- }
1325
- }
220
+ // ─── Install: GitHub Copilot ─────────────────────────────────────────────────
1326
221
 
1327
- // Ensure permission structure exists
1328
- if (!config.permission) {
1329
- config.permission = {};
1330
- }
222
+ function installCopilot() {
223
+ console.log(c.bold(' Installing for GitHub Copilot...'));
224
+ console.log(c.cyan(` Target: ${TARGET}`));
225
+ console.log('');
226
+ if (force) console.log(c.yellow(' --force: existing files will be overwritten\n'));
1331
227
 
1332
- // Build the GSD path using the actual config directory
1333
- // Use ~ shorthand if it's in the default location, otherwise use full path
1334
- const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
1335
- const gsdPath = opencodeConfigDir === defaultConfigDir
1336
- ? '~/.config/opencode/get-shit-done/*'
1337
- : `${opencodeConfigDir.replace(/\\/g, '/')}/get-shit-done/*`;
1338
-
1339
- let modified = false;
1340
-
1341
- // Configure read permission
1342
- if (!config.permission.read || typeof config.permission.read !== 'object') {
1343
- config.permission.read = {};
1344
- }
1345
- if (config.permission.read[gsdPath] !== 'allow') {
1346
- config.permission.read[gsdPath] = 'allow';
1347
- modified = true;
1348
- }
228
+ const results = [];
1349
229
 
1350
- // Configure external_directory permission (the safety guard for paths outside project)
1351
- if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
1352
- config.permission.external_directory = {};
1353
- }
1354
- if (config.permission.external_directory[gsdPath] !== 'allow') {
1355
- config.permission.external_directory[gsdPath] = 'allow';
1356
- modified = true;
230
+ // 1. copilot-instructions.md
231
+ if (fs.existsSync(SRC.copilotInstructions)) {
232
+ const dest = path.join(TARGET, '.github', 'copilot-instructions.md');
233
+ results.push({ dest, status: copyFile(SRC.copilotInstructions, dest) });
234
+ } else {
235
+ console.error(c.red(' Error: source not found: ' + SRC.copilotInstructions));
236
+ process.exit(1);
1357
237
  }
1358
238
 
1359
- if (!modified) {
1360
- return; // Already configured
239
+ // 2. .github/prompts/
240
+ if (fs.existsSync(SRC.copilotPrompts)) {
241
+ copyDir(SRC.copilotPrompts, path.join(TARGET, '.github', 'prompts'), results);
1361
242
  }
1362
243
 
1363
- // Write config back
1364
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
1365
- console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
1366
- }
1367
-
1368
- /**
1369
- * Verify a directory exists and contains files
1370
- */
1371
- function verifyInstalled(dirPath, description) {
1372
- if (!fs.existsSync(dirPath)) {
1373
- console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
1374
- return false;
244
+ // 3. .github/instructions/
245
+ if (fs.existsSync(SRC.copilotInstructDir)) {
246
+ copyDir(SRC.copilotInstructDir, path.join(TARGET, '.github', 'instructions'), results);
1375
247
  }
1376
- try {
1377
- const entries = fs.readdirSync(dirPath);
1378
- if (entries.length === 0) {
1379
- console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
1380
- return false;
1381
- }
1382
- } catch (e) {
1383
- console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
1384
- return false;
1385
- }
1386
- return true;
1387
- }
1388
248
 
1389
- /**
1390
- * Verify a file exists
1391
- */
1392
- function verifyFileInstalled(filePath, description) {
1393
- if (!fs.existsSync(filePath)) {
1394
- console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
1395
- return false;
249
+ // 4. .github/agents/
250
+ if (fs.existsSync(SRC.copilotAgents)) {
251
+ copyDir(SRC.copilotAgents, path.join(TARGET, '.github', 'agents'), results);
1396
252
  }
1397
- return true;
1398
- }
1399
253
 
1400
- /**
1401
- * Install to the specified directory for a specific runtime
1402
- * @param {boolean} isGlobal - Whether to install globally or locally
1403
- * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex')
1404
- */
1405
-
1406
- // ──────────────────────────────────────────────────────
1407
- // Local Patch Persistence
1408
- // ──────────────────────────────────────────────────────
1409
-
1410
- const PATCHES_DIR_NAME = 'gsd-local-patches';
1411
- const MANIFEST_NAME = 'gsd-file-manifest.json';
1412
-
1413
- /**
1414
- * Compute SHA256 hash of file contents
1415
- */
1416
- function fileHash(filePath) {
1417
- const content = fs.readFileSync(filePath);
1418
- return crypto.createHash('sha256').update(content).digest('hex');
1419
- }
1420
-
1421
- /**
1422
- * Recursively collect all files in dir with their hashes
1423
- */
1424
- function generateManifest(dir, baseDir) {
1425
- if (!baseDir) baseDir = dir;
1426
- const manifest = {};
1427
- if (!fs.existsSync(dir)) return manifest;
1428
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1429
- for (const entry of entries) {
1430
- const fullPath = path.join(dir, entry.name);
1431
- const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
1432
- if (entry.isDirectory()) {
1433
- Object.assign(manifest, generateManifest(fullPath, baseDir));
1434
- } else {
1435
- manifest[relPath] = fileHash(fullPath);
1436
- }
254
+ // 5. .planning/
255
+ if (fs.existsSync(SRC.planning)) {
256
+ copyDir(SRC.planning, path.join(TARGET, '.planning'), results);
257
+ } else {
258
+ console.error(c.red(' Error: source not found: ' + SRC.planning));
259
+ process.exit(1);
1437
260
  }
1438
- return manifest;
1439
- }
1440
261
 
1441
- /**
1442
- * Write file manifest after installation for future modification detection
1443
- */
1444
- function writeManifest(configDir, runtime = 'claude') {
1445
- const isOpencode = runtime === 'opencode';
1446
- const isCodex = runtime === 'codex';
1447
- const gsdDir = path.join(configDir, 'get-shit-done');
1448
- const commandsDir = path.join(configDir, 'commands', 'gsd');
1449
- const opencodeCommandDir = path.join(configDir, 'command');
1450
- const codexSkillsDir = path.join(configDir, 'skills');
1451
- const agentsDir = path.join(configDir, 'agents');
1452
- const manifest = { version: pkg.version, timestamp: new Date().toISOString(), files: {} };
1453
-
1454
- const gsdHashes = generateManifest(gsdDir);
1455
- for (const [rel, hash] of Object.entries(gsdHashes)) {
1456
- manifest.files['get-shit-done/' + rel] = hash;
1457
- }
1458
- if (!isOpencode && !isCodex && fs.existsSync(commandsDir)) {
1459
- const cmdHashes = generateManifest(commandsDir);
1460
- for (const [rel, hash] of Object.entries(cmdHashes)) {
1461
- manifest.files['commands/gsd/' + rel] = hash;
1462
- }
1463
- }
1464
- if (isOpencode && fs.existsSync(opencodeCommandDir)) {
1465
- for (const file of fs.readdirSync(opencodeCommandDir)) {
1466
- if (file.startsWith('gsd-') && file.endsWith('.md')) {
1467
- manifest.files['command/' + file] = fileHash(path.join(opencodeCommandDir, file));
1468
- }
1469
- }
1470
- }
1471
- if (isCodex && fs.existsSync(codexSkillsDir)) {
1472
- for (const skillName of listCodexSkillNames(codexSkillsDir)) {
1473
- const skillRoot = path.join(codexSkillsDir, skillName);
1474
- const skillHashes = generateManifest(skillRoot);
1475
- for (const [rel, hash] of Object.entries(skillHashes)) {
1476
- manifest.files[`skills/${skillName}/${rel}`] = hash;
1477
- }
1478
- }
1479
- }
1480
- if (fs.existsSync(agentsDir)) {
1481
- for (const file of fs.readdirSync(agentsDir)) {
1482
- if (file.startsWith('gsd-') && file.endsWith('.md')) {
1483
- manifest.files['agents/' + file] = fileHash(path.join(agentsDir, file));
1484
- }
1485
- }
1486
- }
262
+ // Summary
263
+ console.log(c.bold(' Files:'));
264
+ const totals = printResults(results);
265
+ console.log('');
266
+ console.log(
267
+ c.green(' Done') + ' ' +
268
+ `${totals.copied} copied, ${totals.skipped} skipped, ${totals.overwritten} overwritten`
269
+ );
1487
270
 
1488
- fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
1489
- return manifest;
271
+ printCopilotQuickStart();
1490
272
  }
1491
273
 
1492
- /**
1493
- * Detect user-modified GSD files by comparing against install manifest.
1494
- * Backs up modified files to gsd-local-patches/ for reapply after update.
1495
- */
1496
- function saveLocalPatches(configDir) {
1497
- const manifestPath = path.join(configDir, MANIFEST_NAME);
1498
- if (!fs.existsSync(manifestPath)) return [];
1499
-
1500
- let manifest;
1501
- try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
1502
-
1503
- const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
1504
- const modified = [];
1505
-
1506
- for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
1507
- const fullPath = path.join(configDir, relPath);
1508
- if (!fs.existsSync(fullPath)) continue;
1509
- const currentHash = fileHash(fullPath);
1510
- if (currentHash !== originalHash) {
1511
- const backupPath = path.join(patchesDir, relPath);
1512
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
1513
- fs.copyFileSync(fullPath, backupPath);
1514
- modified.push(relPath);
1515
- }
1516
- }
274
+ // ─── Install: OpenCode ───────────────────────────────────────────────────────
1517
275
 
1518
- if (modified.length > 0) {
1519
- const meta = {
1520
- backed_up_at: new Date().toISOString(),
1521
- from_version: manifest.version,
1522
- files: modified
1523
- };
1524
- fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
1525
- console.log(' ' + yellow + 'i' + reset + ' Found ' + modified.length + ' locally modified GSD file(s) — backed up to ' + PATCHES_DIR_NAME + '/');
1526
- for (const f of modified) {
1527
- console.log(' ' + dim + f + reset);
1528
- }
1529
- }
1530
- return modified;
1531
- }
276
+ function installOpencode() {
277
+ console.log(c.bold(' Installing for OpenCode...'));
278
+ console.log(c.cyan(` Target: ${TARGET}`));
279
+ console.log('');
280
+ if (force) console.log(c.yellow(' --force: existing files will be overwritten\n'));
1532
281
 
1533
- /**
1534
- * After install, report backed-up patches for user to reapply.
1535
- */
1536
- function reportLocalPatches(configDir, runtime = 'claude') {
1537
- const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
1538
- const metaPath = path.join(patchesDir, 'backup-meta.json');
1539
- if (!fs.existsSync(metaPath)) return [];
1540
-
1541
- let meta;
1542
- try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
1543
-
1544
- if (meta.files && meta.files.length > 0) {
1545
- const reapplyCommand = runtime === 'opencode'
1546
- ? '/gsd-reapply-patches'
1547
- : runtime === 'codex'
1548
- ? '$gsd-reapply-patches'
1549
- : '/gsd:reapply-patches';
1550
- console.log('');
1551
- console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
1552
- for (const f of meta.files) {
1553
- console.log(' ' + cyan + f + reset);
1554
- }
1555
- console.log('');
1556
- console.log(' Your modifications are saved in ' + cyan + PATCHES_DIR_NAME + '/' + reset);
1557
- console.log(' Run ' + cyan + reapplyCommand + reset + ' to merge them into the new version.');
1558
- console.log(' Or manually compare and merge the files.');
1559
- console.log('');
1560
- }
1561
- return meta.files || [];
1562
- }
282
+ const results = [];
1563
283
 
1564
- function install(isGlobal, runtime = 'claude') {
1565
- const isOpencode = runtime === 'opencode';
1566
- const isGemini = runtime === 'gemini';
1567
- const isCodex = runtime === 'codex';
1568
- const dirName = getDirName(runtime);
1569
- const src = path.join(__dirname, '..');
1570
-
1571
- // Get the target directory based on runtime and install type
1572
- const targetDir = isGlobal
1573
- ? getGlobalDir(runtime, explicitConfigDir)
1574
- : path.join(process.cwd(), dirName);
1575
-
1576
- const locationLabel = isGlobal
1577
- ? targetDir.replace(os.homedir(), '~')
1578
- : targetDir.replace(process.cwd(), '.');
1579
-
1580
- // Path prefix for file references in markdown content
1581
- // For global installs: use full path
1582
- // For local installs: use relative
1583
- const pathPrefix = isGlobal
1584
- ? `${targetDir.replace(/\\/g, '/')}/`
1585
- : `./${dirName}/`;
1586
-
1587
- let runtimeLabel = 'Claude Code';
1588
- if (isOpencode) runtimeLabel = 'OpenCode';
1589
- if (isGemini) runtimeLabel = 'Gemini';
1590
- if (isCodex) runtimeLabel = 'Codex';
1591
-
1592
- console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
1593
-
1594
- // Track installation failures
1595
- const failures = [];
1596
-
1597
- // Save any locally modified GSD files before they get wiped
1598
- saveLocalPatches(targetDir);
1599
-
1600
- // Clean up orphaned files from previous versions
1601
- cleanupOrphanedFiles(targetDir);
1602
-
1603
- // OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/gsd/
1604
- if (isOpencode) {
1605
- // OpenCode: flat structure in command/ directory
1606
- const commandDir = path.join(targetDir, 'command');
1607
- fs.mkdirSync(commandDir, { recursive: true });
1608
-
1609
- // Copy commands/gsd/*.md as command/gsd-*.md (flatten structure)
1610
- const gsdSrc = path.join(src, 'commands', 'gsd');
1611
- copyFlattenedCommands(gsdSrc, commandDir, 'gsd', pathPrefix, runtime);
1612
- if (verifyInstalled(commandDir, 'command/gsd-*')) {
1613
- const count = fs.readdirSync(commandDir).filter(f => f.startsWith('gsd-')).length;
1614
- console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
1615
- } else {
1616
- failures.push('command/gsd-*');
1617
- }
1618
- } else if (isCodex) {
1619
- const skillsDir = path.join(targetDir, 'skills');
1620
- const gsdSrc = path.join(src, 'commands', 'gsd');
1621
- copyCommandsAsCodexSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime);
1622
- const installedSkillNames = listCodexSkillNames(skillsDir);
1623
- if (installedSkillNames.length > 0) {
1624
- console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
1625
- } else {
1626
- failures.push('skills/gsd-*');
1627
- }
1628
- } else {
1629
- // Claude Code & Gemini: nested structure in commands/ directory
1630
- const commandsDir = path.join(targetDir, 'commands');
1631
- fs.mkdirSync(commandsDir, { recursive: true });
1632
-
1633
- const gsdSrc = path.join(src, 'commands', 'gsd');
1634
- const gsdDest = path.join(commandsDir, 'gsd');
1635
- copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true);
1636
- if (verifyInstalled(gsdDest, 'commands/gsd')) {
1637
- console.log(` ${green}✓${reset} Installed commands/gsd`);
1638
- } else {
1639
- failures.push('commands/gsd');
1640
- }
284
+ // 1. AGENTS.md project root
285
+ if (fs.existsSync(SRC.opencodeAgentsMd)) {
286
+ const dest = path.join(TARGET, 'AGENTS.md');
287
+ results.push({ dest, status: copyFile(SRC.opencodeAgentsMd, dest) });
1641
288
  }
1642
289
 
1643
- // Copy get-shit-done skill with path replacement
1644
- const skillSrc = path.join(src, 'get-shit-done');
1645
- const skillDest = path.join(targetDir, 'get-shit-done');
1646
- copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime);
1647
- if (verifyInstalled(skillDest, 'get-shit-done')) {
1648
- console.log(` ${green}✓${reset} Installed get-shit-done`);
1649
- } else {
1650
- failures.push('get-shit-done');
290
+ // 2. opencode.json project root (merge, don't overwrite)
291
+ if (fs.existsSync(SRC.opencodeJson)) {
292
+ const dest = path.join(TARGET, 'opencode.json');
293
+ const status = mergeOpencodeJson(SRC.opencodeJson, dest);
294
+ results.push({ dest, status });
1651
295
  }
1652
296
 
1653
- // Copy agents to agents directory
1654
- const agentsSrc = path.join(src, 'agents');
1655
- if (fs.existsSync(agentsSrc)) {
1656
- const agentsDest = path.join(targetDir, 'agents');
1657
- fs.mkdirSync(agentsDest, { recursive: true });
1658
-
1659
- // Remove old GSD agents (gsd-*.md) before copying new ones
1660
- if (fs.existsSync(agentsDest)) {
1661
- for (const file of fs.readdirSync(agentsDest)) {
1662
- if (file.startsWith('gsd-') && file.endsWith('.md')) {
1663
- fs.unlinkSync(path.join(agentsDest, file));
1664
- }
1665
- }
1666
- }
1667
-
1668
- // Copy new agents
1669
- const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
1670
- for (const entry of agentEntries) {
1671
- if (entry.isFile() && entry.name.endsWith('.md')) {
1672
- let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
1673
- // Always replace ~/.claude/ as it is the source of truth in the repo
1674
- const dirRegex = /~\/\.claude\//g;
1675
- content = content.replace(dirRegex, pathPrefix);
1676
- content = processAttribution(content, getCommitAttribution(runtime));
1677
- // Convert frontmatter for runtime compatibility
1678
- if (isOpencode) {
1679
- content = convertClaudeToOpencodeFrontmatter(content);
1680
- } else if (isGemini) {
1681
- content = convertClaudeToGeminiAgent(content);
1682
- } else if (isCodex) {
1683
- content = convertClaudeToCodexMarkdown(content);
1684
- }
1685
- fs.writeFileSync(path.join(agentsDest, entry.name), content);
1686
- }
1687
- }
1688
- if (verifyInstalled(agentsDest, 'agents')) {
1689
- console.log(` ${green}✓${reset} Installed agents`);
1690
- } else {
1691
- failures.push('agents');
1692
- }
297
+ // 3. .opencode/agents/
298
+ if (fs.existsSync(SRC.opencodeAgents)) {
299
+ copyDir(SRC.opencodeAgents, path.join(TARGET, '.opencode', 'agents'), results);
1693
300
  }
1694
301
 
1695
- // Copy CHANGELOG.md
1696
- const changelogSrc = path.join(src, 'CHANGELOG.md');
1697
- const changelogDest = path.join(targetDir, 'get-shit-done', 'CHANGELOG.md');
1698
- if (fs.existsSync(changelogSrc)) {
1699
- fs.copyFileSync(changelogSrc, changelogDest);
1700
- if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
1701
- console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
1702
- } else {
1703
- failures.push('CHANGELOG.md');
1704
- }
302
+ // 4. .opencode/commands/
303
+ if (fs.existsSync(SRC.opencodeCommands)) {
304
+ copyDir(SRC.opencodeCommands, path.join(TARGET, '.opencode', 'commands'), results);
1705
305
  }
1706
306
 
1707
- // Write VERSION file
1708
- const versionDest = path.join(targetDir, 'get-shit-done', 'VERSION');
1709
- fs.writeFileSync(versionDest, pkg.version);
1710
- if (verifyFileInstalled(versionDest, 'VERSION')) {
1711
- console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
307
+ // 5. .planning/
308
+ if (fs.existsSync(SRC.planning)) {
309
+ copyDir(SRC.planning, path.join(TARGET, '.planning'), results);
1712
310
  } else {
1713
- failures.push('VERSION');
1714
- }
1715
-
1716
- if (!isCodex) {
1717
- // Write package.json to force CommonJS mode for GSD scripts
1718
- // Prevents "require is not defined" errors when project has "type": "module"
1719
- // Node.js walks up looking for package.json - this stops inheritance from project
1720
- const pkgJsonDest = path.join(targetDir, 'package.json');
1721
- fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
1722
- console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
1723
-
1724
- // Copy hooks from dist/ (bundled with dependencies)
1725
- // Template paths for the target runtime (replaces '.claude' with correct config dir)
1726
- const hooksSrc = path.join(src, 'hooks', 'dist');
1727
- if (fs.existsSync(hooksSrc)) {
1728
- const hooksDest = path.join(targetDir, 'hooks');
1729
- fs.mkdirSync(hooksDest, { recursive: true });
1730
- const hookEntries = fs.readdirSync(hooksSrc);
1731
- const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
1732
- for (const entry of hookEntries) {
1733
- const srcFile = path.join(hooksSrc, entry);
1734
- if (fs.statSync(srcFile).isFile()) {
1735
- const destFile = path.join(hooksDest, entry);
1736
- // Template .js files to replace '.claude' with runtime-specific config dir
1737
- if (entry.endsWith('.js')) {
1738
- let content = fs.readFileSync(srcFile, 'utf8');
1739
- content = content.replace(/'\.claude'/g, configDirReplacement);
1740
- fs.writeFileSync(destFile, content);
1741
- } else {
1742
- fs.copyFileSync(srcFile, destFile);
1743
- }
1744
- }
1745
- }
1746
- if (verifyInstalled(hooksDest, 'hooks')) {
1747
- console.log(` ${green}✓${reset} Installed hooks (bundled)`);
1748
- } else {
1749
- failures.push('hooks');
1750
- }
1751
- }
1752
- }
1753
-
1754
- if (failures.length > 0) {
1755
- console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
311
+ console.error(c.red(' Error: source not found: ' + SRC.planning));
1756
312
  process.exit(1);
1757
313
  }
1758
314
 
1759
- // Write file manifest for future modification detection
1760
- writeManifest(targetDir, runtime);
1761
- console.log(` ${green}✓${reset} Wrote file manifest (${MANIFEST_NAME})`);
1762
-
1763
- // Report any backed-up local patches
1764
- reportLocalPatches(targetDir, runtime);
1765
-
1766
- if (isCodex) {
1767
- return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
1768
- }
1769
-
1770
- // Configure statusline and hooks in settings.json
1771
- // Gemini shares same hook system as Claude Code for now
1772
- const settingsPath = path.join(targetDir, 'settings.json');
1773
- const settings = cleanupOrphanedHooks(readSettings(settingsPath));
1774
- const statuslineCommand = isGlobal
1775
- ? buildHookCommand(targetDir, 'gsd-statusline.js')
1776
- : 'node ' + dirName + '/hooks/gsd-statusline.js';
1777
- const updateCheckCommand = isGlobal
1778
- ? buildHookCommand(targetDir, 'gsd-check-update.js')
1779
- : 'node ' + dirName + '/hooks/gsd-check-update.js';
1780
- const contextMonitorCommand = isGlobal
1781
- ? buildHookCommand(targetDir, 'gsd-context-monitor.js')
1782
- : 'node ' + dirName + '/hooks/gsd-context-monitor.js';
1783
-
1784
- // Enable experimental agents for Gemini CLI (required for custom sub-agents)
1785
- if (isGemini) {
1786
- if (!settings.experimental) {
1787
- settings.experimental = {};
1788
- }
1789
- if (!settings.experimental.enableAgents) {
1790
- settings.experimental.enableAgents = true;
1791
- console.log(` ${green}✓${reset} Enabled experimental agents`);
1792
- }
1793
- }
1794
-
1795
- // Configure SessionStart hook for update checking (skip for opencode)
1796
- if (!isOpencode) {
1797
- if (!settings.hooks) {
1798
- settings.hooks = {};
1799
- }
1800
- if (!settings.hooks.SessionStart) {
1801
- settings.hooks.SessionStart = [];
1802
- }
1803
-
1804
- const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
1805
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update'))
1806
- );
1807
-
1808
- if (!hasGsdUpdateHook) {
1809
- settings.hooks.SessionStart.push({
1810
- hooks: [
1811
- {
1812
- type: 'command',
1813
- command: updateCheckCommand
1814
- }
1815
- ]
1816
- });
1817
- console.log(` ${green}✓${reset} Configured update check hook`);
1818
- }
315
+ // Summary
316
+ console.log(c.bold(' Files:'));
317
+ const totals = printResults(results);
318
+ console.log('');
319
+ console.log(
320
+ c.green(' Done') + ' ' +
321
+ `${totals.copied} copied, ${totals.skipped} skipped, ${totals.overwritten} overwritten`
322
+ );
323
+
324
+ printOpencodeQuickStart();
325
+ }
326
+
327
+ // ─── Uninstall ───────────────────────────────────────────────────────────────
328
+
329
+ function uninstallCopilot() {
330
+ console.log(c.bold(' Uninstalling GitHub Copilot context...'));
331
+ let removed = 0;
332
+
333
+ if (removeFile(path.join(TARGET, '.github', 'copilot-instructions.md'))) removed++;
334
+ if (removeDir(path.join(TARGET, '.github', 'prompts'))) removed++;
335
+ if (removeDir(path.join(TARGET, '.github', 'instructions'))) removed++;
336
+ if (removeDir(path.join(TARGET, '.github', 'agents'))) removed++;
337
+
338
+ console.log(removed > 0
339
+ ? c.green(` ✓ Removed ${removed} Copilot items`)
340
+ : c.dim(' Nothing to remove'));
341
+ }
342
+
343
+ function uninstallOpencode() {
344
+ console.log(c.bold(' Uninstalling OpenCode context...'));
345
+ let removed = 0;
346
+
347
+ if (removeFile(path.join(TARGET, 'AGENTS.md'))) removed++;
348
+ if (removeFile(path.join(TARGET, 'opencode.json'))) removed++;
349
+ if (removeDir(path.join(TARGET, '.opencode', 'agents'))) removed++;
350
+ if (removeDir(path.join(TARGET, '.opencode', 'commands'))) removed++;
351
+
352
+ console.log(removed > 0
353
+ ? c.green(` ✓ Removed ${removed} OpenCode items`)
354
+ : c.dim(' Nothing to remove'));
355
+ }
356
+
357
+ function uninstallPlanning() {
358
+ if (removeDir(path.join(TARGET, '.planning'))) {
359
+ console.log(c.green(' ✓ Removed .planning/'));
360
+ }
361
+ }
362
+
363
+ // ─── Quick Start Guides ──────────────────────────────────────────────────────
364
+
365
+ function printCopilotQuickStart() {
366
+ console.log('');
367
+ console.log(c.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
368
+ console.log(c.bold(' Quick Start — GitHub Copilot'));
369
+ console.log(c.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
370
+ console.log('');
371
+ console.log(c.cyan(' SETUP') + c.dim(' (run once in Copilot Chat):'));
372
+ console.log(` ${c.green('/new-project')} Initialize project → roadmap`);
373
+ console.log(` ${c.green('/map-codebase')} Analyze code 7 context docs`);
374
+ console.log('');
375
+ console.log(c.cyan(' AGENTS') + c.dim(' (select from agent dropdown):'));
376
+ console.log(` ${c.green('@ProductOwner')} Defines specs, creates Jira tickets`);
377
+ console.log(` ${c.green('@Planner')} Plans work from Jira/roadmap`);
378
+ console.log(` ${c.green('@Executor')} Implements plans, updates Jira`);
379
+ console.log(` ${c.green('@Verifier')} Checks work, reports to Jira`);
380
+ console.log('');
381
+ console.log(c.cyan(' WORKFLOW:'));
382
+ console.log(` ${c.green('1.')} @Planner "Plan phase 1" → detailed steps`);
383
+ console.log(` ${c.green('2.')} @Executor → implements plan`);
384
+ console.log(` ${c.green('3.')} @Verifier → checks work`);
385
+ console.log('');
386
+ console.log(c.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
387
+ console.log(c.dim(' Docs: .github/copilot-instructions.md'));
388
+ console.log('');
389
+ }
390
+
391
+ function printOpencodeQuickStart() {
392
+ console.log('');
393
+ console.log(c.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
394
+ console.log(c.bold(' Quick Start — OpenCode'));
395
+ console.log(c.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
396
+ console.log('');
397
+ console.log(c.cyan(' SETUP') + c.dim(' (run once in OpenCode):'));
398
+ console.log(` ${c.green('/new-project')} Initialize project → roadmap`);
399
+ console.log(` ${c.green('/map-codebase')} Analyze code → 7 context docs`);
400
+ console.log('');
401
+ console.log(c.cyan(' AGENTS') + c.dim(' (switch via tab or @mention):'));
402
+ console.log(` ${c.green('@product-owner')} Defines specs, creates Jira tickets`);
403
+ console.log(` ${c.green('@planner')} Plans work from Jira/roadmap`);
404
+ console.log(` ${c.green('@executor')} Implements plans ${c.dim('(default)')}`);
405
+ console.log(` ${c.green('@verifier')} Checks work`);
406
+ console.log('');
407
+ console.log(c.cyan(' WORKFLOW:'));
408
+ console.log(` ${c.green('1.')} @planner "Plan phase 1" → detailed steps`);
409
+ console.log(` ${c.green('2.')} @executor → implements plan`);
410
+ console.log(` ${c.green('3.')} @verifier → checks work`);
411
+ console.log('');
412
+ console.log(c.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
413
+ console.log(c.dim(' Docs: AGENTS.md'));
414
+ console.log('');
415
+ }
416
+
417
+ // ─── Help ────────────────────────────────────────────────────────────────────
418
+
419
+ function showHelp() {
420
+ console.log(`
421
+ ${c.yellow('Usage:')} npx get-shit-done-cc [options] [target-dir]
1819
422
 
1820
- // Configure PostToolUse hook for context window monitoring
1821
- if (!settings.hooks.PostToolUse) {
1822
- settings.hooks.PostToolUse = [];
1823
- }
423
+ ${c.yellow('Options:')}
424
+ ${c.cyan('--copilot')} Install for GitHub Copilot (VS Code)
425
+ ${c.cyan('--opencode')} Install for OpenCode (terminal)
426
+ ${c.cyan('--both')} Install for both
427
+ ${c.cyan('--force, -f')} Overwrite existing files
428
+ ${c.cyan('--uninstall, -u')} Remove GSD files
429
+ ${c.cyan('--help, -h')} Show this help
1824
430
 
1825
- const hasContextMonitorHook = settings.hooks.PostToolUse.some(entry =>
1826
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-context-monitor'))
1827
- );
1828
-
1829
- if (!hasContextMonitorHook) {
1830
- settings.hooks.PostToolUse.push({
1831
- hooks: [
1832
- {
1833
- type: 'command',
1834
- command: contextMonitorCommand
1835
- }
1836
- ]
1837
- });
1838
- console.log(` ${green}✓${reset} Configured context window monitor hook`);
1839
- }
1840
- }
431
+ ${c.yellow('Examples:')}
432
+ ${c.dim('# Interactive (prompts for target)')}
433
+ npx get-shit-done-cc
1841
434
 
1842
- return { settingsPath, settings, statuslineCommand, runtime };
1843
- }
435
+ ${c.dim('# Install for Copilot in current project')}
436
+ npx get-shit-done-cc --copilot
1844
437
 
1845
- /**
1846
- * Apply statusline config, then print completion message
1847
- */
1848
- function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) {
1849
- const isOpencode = runtime === 'opencode';
1850
- const isCodex = runtime === 'codex';
1851
-
1852
- if (shouldInstallStatusline && !isOpencode && !isCodex) {
1853
- settings.statusLine = {
1854
- type: 'command',
1855
- command: statuslineCommand
1856
- };
1857
- console.log(` ${green}✓${reset} Configured statusline`);
1858
- }
438
+ ${c.dim('# Install for OpenCode in current project')}
439
+ npx get-shit-done-cc --opencode
1859
440
 
1860
- // Write settings when runtime supports settings.json
1861
- if (!isCodex) {
1862
- writeSettings(settingsPath, settings);
1863
- }
441
+ ${c.dim('# Install both into a specific directory')}
442
+ npx get-shit-done-cc --both /path/to/project
1864
443
 
1865
- // Configure OpenCode permissions
1866
- if (isOpencode) {
1867
- configureOpencodePermissions(isGlobal);
1868
- }
444
+ ${c.dim('# Force overwrite existing files')}
445
+ npx get-shit-done-cc --copilot --force
1869
446
 
1870
- let program = 'Claude Code';
1871
- if (runtime === 'opencode') program = 'OpenCode';
1872
- if (runtime === 'gemini') program = 'Gemini';
1873
- if (runtime === 'codex') program = 'Codex';
1874
-
1875
- let command = '/gsd:help';
1876
- if (runtime === 'opencode') command = '/gsd-help';
1877
- if (runtime === 'codex') command = '$gsd-help';
1878
- console.log(`
1879
- ${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
1880
-
1881
- ${cyan}Join the community:${reset} https://discord.gg/5JJgD5svVS
447
+ ${c.dim('# Remove GSD files for both targets')}
448
+ npx get-shit-done-cc --both --uninstall
1882
449
  `);
1883
450
  }
1884
451
 
1885
- /**
1886
- * Handle statusline configuration with optional prompt
1887
- */
1888
- function handleStatusline(settings, isInteractive, callback) {
1889
- const hasExisting = settings.statusLine != null;
1890
-
1891
- if (!hasExisting) {
1892
- callback(true);
1893
- return;
1894
- }
452
+ // ─── Interactive prompt ──────────────────────────────────────────────────────
1895
453
 
1896
- if (forceStatusline) {
1897
- callback(true);
1898
- return;
1899
- }
1900
-
1901
- if (!isInteractive) {
1902
- console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
1903
- console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
1904
- callback(false);
454
+ function promptTarget(callback) {
455
+ if (!process.stdin.isTTY) {
456
+ console.log(c.yellow(' Non-interactive terminal — defaulting to both targets\n'));
457
+ callback(['copilot', 'opencode']);
1905
458
  return;
1906
459
  }
1907
460
 
1908
- const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
1909
-
1910
461
  const rl = readline.createInterface({
1911
462
  input: process.stdin,
1912
- output: process.stdout
1913
- });
1914
-
1915
- console.log(`
1916
- ${yellow}⚠${reset} Existing statusline detected\n
1917
- Your current statusline:
1918
- ${dim}command: ${existingCmd}${reset}
1919
-
1920
- GSD includes a statusline showing:
1921
- • Model name
1922
- • Current task (from todo list)
1923
- • Context window usage (color-coded)
1924
-
1925
- ${cyan}1${reset}) Keep existing
1926
- ${cyan}2${reset}) Replace with GSD statusline
1927
- `);
1928
-
1929
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
1930
- rl.close();
1931
- const choice = answer.trim() || '1';
1932
- callback(choice === '2');
1933
- });
1934
- }
1935
-
1936
- /**
1937
- * Prompt for runtime selection
1938
- */
1939
- function promptRuntime(callback) {
1940
- const rl = readline.createInterface({
1941
- input: process.stdin,
1942
- output: process.stdout
463
+ output: process.stdout,
1943
464
  });
1944
465
 
1945
466
  let answered = false;
@@ -1947,152 +468,68 @@ function promptRuntime(callback) {
1947
468
  rl.on('close', () => {
1948
469
  if (!answered) {
1949
470
  answered = true;
1950
- console.log(`\n ${yellow}Installation cancelled${reset}\n`);
471
+ console.log(`\n ${c.yellow('Installation cancelled')}\n`);
1951
472
  process.exit(0);
1952
473
  }
1953
474
  });
1954
475
 
1955
- console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
1956
- ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
1957
- ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
1958
- ${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
1959
- ${cyan}5${reset}) All
1960
- ${cyan}6${reset}) GitHub Copilot ${dim}(VS Code — .github/ context system)${reset}
1961
- `);
476
+ console.log(` ${c.yellow('Which target(s) do you want to install for?')}\n`);
477
+ console.log(` ${c.cyan('1')}${c.dim(')')} GitHub Copilot ${c.dim('.github/prompts, agents, instructions')}`);
478
+ console.log(` ${c.cyan('2')}${c.dim(')')} OpenCode ${c.dim('AGENTS.md, .opencode/agents, commands')}`);
479
+ console.log(` ${c.cyan('3')}${c.dim(')')} Both`);
480
+ console.log('');
1962
481
 
1963
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
482
+ rl.question(` Choice ${c.dim('[3]')}: `, (answer) => {
1964
483
  answered = true;
1965
484
  rl.close();
1966
- const choice = answer.trim() || '1';
1967
- if (choice === '6') {
1968
- require('./setup-copilot-context');
1969
- } else if (choice === '5') {
1970
- callback(['claude', 'opencode', 'gemini', 'codex']);
1971
- } else if (choice === '4') {
1972
- callback(['codex']);
1973
- } else if (choice === '3') {
1974
- callback(['gemini']);
1975
- } else if (choice === '2') {
1976
- callback(['opencode']);
1977
- } else {
1978
- callback(['claude']);
1979
- }
485
+ const choice = answer.trim() || '3';
486
+ if (choice === '1') callback(['copilot']);
487
+ else if (choice === '2') callback(['opencode']);
488
+ else callback(['copilot', 'opencode']);
1980
489
  });
1981
490
  }
1982
491
 
1983
- /**
1984
- * Prompt for install location
1985
- */
1986
- function promptLocation(runtimes) {
1987
- if (!process.stdin.isTTY) {
1988
- console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
1989
- installAllRuntimes(runtimes, true, false);
1990
- return;
1991
- }
492
+ // ─── Main ────────────────────────────────────────────────────────────────────
1992
493
 
1993
- const rl = readline.createInterface({
1994
- input: process.stdin,
1995
- output: process.stdout
1996
- });
1997
-
1998
- let answered = false;
1999
-
2000
- rl.on('close', () => {
2001
- if (!answered) {
2002
- answered = true;
2003
- console.log(`\n ${yellow}Installation cancelled${reset}\n`);
2004
- process.exit(0);
2005
- }
2006
- });
2007
-
2008
- const pathExamples = runtimes.map(r => {
2009
- const globalPath = getGlobalDir(r, explicitConfigDir);
2010
- return globalPath.replace(os.homedir(), '~');
2011
- }).join(', ');
2012
-
2013
- const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
2014
-
2015
- console.log(` ${yellow}Where would you like to install?${reset}\n\n ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
2016
- ${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
2017
- `);
494
+ console.log(banner);
2018
495
 
2019
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
2020
- answered = true;
2021
- rl.close();
2022
- const choice = answer.trim() || '1';
2023
- const isGlobal = choice !== '2';
2024
- installAllRuntimes(runtimes, isGlobal, true);
2025
- });
496
+ if (hasHelp) {
497
+ showHelp();
498
+ process.exit(0);
2026
499
  }
2027
500
 
2028
- /**
2029
- * Install GSD for all selected runtimes
2030
- */
2031
- function installAllRuntimes(runtimes, isGlobal, isInteractive) {
2032
- const results = [];
2033
-
2034
- for (const runtime of runtimes) {
2035
- const result = install(isGlobal, runtime);
2036
- results.push(result);
2037
- }
501
+ // Build target list from flags
502
+ let targets = [];
503
+ if (hasBoth) targets = ['copilot', 'opencode'];
504
+ else if (hasCopilot && hasOpencode) targets = ['copilot', 'opencode'];
505
+ else if (hasCopilot) targets = ['copilot'];
506
+ else if (hasOpencode) targets = ['opencode'];
2038
507
 
2039
- const statuslineRuntimes = ['claude', 'gemini'];
2040
- const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
2041
-
2042
- const finalize = (shouldInstallStatusline) => {
2043
- for (const result of results) {
2044
- const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
2045
- finishInstall(
2046
- result.settingsPath,
2047
- result.settings,
2048
- result.statuslineCommand,
2049
- useStatusline,
2050
- result.runtime,
2051
- isGlobal
2052
- );
508
+ function run(targets) {
509
+ if (hasUninstall) {
510
+ // Uninstall
511
+ for (const target of targets) {
512
+ if (target === 'copilot') uninstallCopilot();
513
+ if (target === 'opencode') uninstallOpencode();
2053
514
  }
2054
- };
2055
-
2056
- if (primaryStatuslineResult) {
2057
- handleStatusline(primaryStatuslineResult.settings, isInteractive, finalize);
515
+ uninstallPlanning();
516
+ console.log('');
517
+ console.log(c.green(' Uninstall complete'));
518
+ console.log('');
2058
519
  } else {
2059
- finalize(false);
520
+ // Install
521
+ for (const target of targets) {
522
+ if (target === 'copilot') installCopilot();
523
+ if (target === 'opencode') installOpencode();
524
+ }
2060
525
  }
2061
526
  }
2062
527
 
2063
- // Main logic
2064
- if (hasGlobal && hasLocal) {
2065
- console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
2066
- process.exit(1);
2067
- } else if (explicitConfigDir && hasLocal) {
2068
- console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
2069
- process.exit(1);
2070
- } else if (hasUninstall) {
2071
- if (!hasGlobal && !hasLocal) {
2072
- console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
2073
- process.exit(1);
2074
- }
2075
- const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
2076
- for (const runtime of runtimes) {
2077
- uninstall(hasGlobal, runtime);
2078
- }
2079
- } else if (selectedRuntimes.length > 0) {
2080
- if (!hasGlobal && !hasLocal) {
2081
- promptLocation(selectedRuntimes);
2082
- } else {
2083
- installAllRuntimes(selectedRuntimes, hasGlobal, false);
2084
- }
2085
- } else if (hasGlobal || hasLocal) {
2086
- // Default to Claude if no runtime specified but location is
2087
- installAllRuntimes(['claude'], hasGlobal, false);
528
+ if (targets.length > 0) {
529
+ run(targets);
2088
530
  } else {
2089
531
  // Interactive
2090
- if (!process.stdin.isTTY) {
2091
- console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
2092
- installAllRuntimes(['claude'], true, false);
2093
- } else {
2094
- promptRuntime((runtimes) => {
2095
- promptLocation(runtimes);
2096
- });
2097
- }
532
+ promptTarget((selected) => {
533
+ run(selected);
534
+ });
2098
535
  }