@ktpartners/dgs-platform 2.6.2

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