@boyingliu01/xp-gate 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/bin/xp-gate.js +4 -9
  2. package/lib/__tests__/audit-log.test.ts +107 -0
  3. package/lib/__tests__/detect-deps.test.js +212 -42
  4. package/lib/__tests__/doctor.test.js +1 -0
  5. package/lib/__tests__/gate-audit.test.ts +59 -0
  6. package/lib/__tests__/init.test.js +47 -1
  7. package/lib/__tests__/install-skill.test.js +1 -45
  8. package/lib/__tests__/ui-detector.test.ts +186 -26
  9. package/lib/__tests__/ui-review.test.ts +119 -0
  10. package/lib/__tests__/uninstall.test.js +1 -0
  11. package/lib/detect-deps.js +180 -37
  12. package/lib/doctor.js +7 -9
  13. package/lib/gate-audit.ts +26 -42
  14. package/lib/init.js +42 -13
  15. package/lib/install-skill.js +20 -48
  16. package/lib/rollback.js +1 -16
  17. package/lib/shared-paths.js +30 -0
  18. package/lib/shared-utils.js +25 -0
  19. package/lib/ui-detector.ts +20 -14
  20. package/lib/ui-review.ts +58 -53
  21. package/lib/uninstall.js +8 -9
  22. package/package.json +1 -1
  23. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +904 -0
  25. package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +45 -0
  26. package/plugins/claude-code/skills/delphi-review/AGENTS.md +66 -0
  27. package/plugins/claude-code/skills/delphi-review/INSTALL.md +152 -0
  28. package/plugins/claude-code/skills/delphi-review/SKILL.md +454 -0
  29. package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +56 -0
  30. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +486 -0
  31. package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +156 -0
  32. package/plugins/claude-code/skills/ralph-loop/SKILL.md +501 -0
  33. package/plugins/claude-code/skills/ralph-loop/references/components/memory.md +55 -0
  34. package/plugins/claude-code/skills/ralph-loop/references/components/middleware.md +54 -0
  35. package/plugins/claude-code/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  36. package/plugins/claude-code/skills/ralph-loop/references/components/system-prompt.md +24 -0
  37. package/plugins/claude-code/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  38. package/plugins/claude-code/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  39. package/plugins/claude-code/skills/ralph-loop/templates/progress-log.md +36 -0
  40. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +68 -0
  41. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1248 -0
  42. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +87 -0
  43. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +72 -0
  44. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  45. package/plugins/claude-code/skills/sprint-flow/references/components/system-prompt.md +27 -0
  46. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  47. package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +203 -0
  48. package/plugins/claude-code/skills/sprint-flow/references/phase-0-think.md +115 -0
  49. package/plugins/claude-code/skills/sprint-flow/references/phase-1-plan.md +178 -0
  50. package/plugins/claude-code/skills/sprint-flow/references/phase-2-build.md +198 -0
  51. package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +213 -0
  52. package/plugins/claude-code/skills/sprint-flow/references/phase-4-uat.md +125 -0
  53. package/plugins/claude-code/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  54. package/plugins/claude-code/skills/sprint-flow/references/phase-6-ship.md +193 -0
  55. package/plugins/claude-code/skills/sprint-flow/references/phase-7-land.md +140 -0
  56. package/plugins/claude-code/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  57. package/plugins/claude-code/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
  58. package/plugins/claude-code/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
  59. package/plugins/claude-code/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
  60. package/plugins/claude-code/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  61. package/plugins/claude-code/skills/sprint-flow/templates/pain-document-template.md +115 -0
  62. package/plugins/claude-code/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
  63. package/plugins/claude-code/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  64. package/plugins/claude-code/skills/test-driven-development/SKILL.md +71 -0
  65. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +56 -0
  66. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +702 -0
  67. package/plugins/claude-code/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  68. package/plugins/claude-code/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  69. package/plugins/claude-code/skills/test-specification-alignment/references/specification-format.md +348 -0
  70. package/plugins/claude-code/skills/to-issues/SKILL.md +277 -0
  71. package/plugins/opencode/README.md +38 -0
  72. package/plugins/opencode/index.ts +85 -0
  73. package/plugins/opencode/package.json +18 -0
  74. package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +904 -0
  75. package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +45 -0
  76. package/plugins/opencode/skills/delphi-review/AGENTS.md +66 -0
  77. package/plugins/opencode/skills/delphi-review/INSTALL.md +152 -0
  78. package/plugins/opencode/skills/delphi-review/SKILL.md +454 -0
  79. package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +56 -0
  80. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +486 -0
  81. package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +156 -0
  82. package/plugins/opencode/skills/ralph-loop/SKILL.md +501 -0
  83. package/plugins/opencode/skills/ralph-loop/references/components/memory.md +55 -0
  84. package/plugins/opencode/skills/ralph-loop/references/components/middleware.md +54 -0
  85. package/plugins/opencode/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  86. package/plugins/opencode/skills/ralph-loop/references/components/system-prompt.md +24 -0
  87. package/plugins/opencode/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  88. package/plugins/opencode/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  89. package/plugins/opencode/skills/ralph-loop/templates/progress-log.md +36 -0
  90. package/plugins/opencode/skills/sprint-flow/AGENTS.md +68 -0
  91. package/plugins/opencode/skills/sprint-flow/SKILL.md +1248 -0
  92. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +87 -0
  93. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +72 -0
  94. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  95. package/plugins/opencode/skills/sprint-flow/references/components/system-prompt.md +27 -0
  96. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  97. package/plugins/opencode/skills/sprint-flow/references/force-levels.md +203 -0
  98. package/plugins/opencode/skills/sprint-flow/references/phase-0-think.md +115 -0
  99. package/plugins/opencode/skills/sprint-flow/references/phase-1-plan.md +178 -0
  100. package/plugins/opencode/skills/sprint-flow/references/phase-2-build.md +198 -0
  101. package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +213 -0
  102. package/plugins/opencode/skills/sprint-flow/references/phase-4-uat.md +125 -0
  103. package/plugins/opencode/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  104. package/plugins/opencode/skills/sprint-flow/references/phase-6-ship.md +193 -0
  105. package/plugins/opencode/skills/sprint-flow/references/phase-7-land.md +140 -0
  106. package/plugins/opencode/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  107. package/plugins/opencode/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
  108. package/plugins/opencode/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
  109. package/plugins/opencode/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
  110. package/plugins/opencode/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  111. package/plugins/opencode/skills/sprint-flow/templates/pain-document-template.md +115 -0
  112. package/plugins/opencode/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
  113. package/plugins/opencode/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  114. package/plugins/opencode/skills/test-driven-development/SKILL.md +71 -0
  115. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +56 -0
  116. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +702 -0
  117. package/plugins/opencode/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  118. package/plugins/opencode/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  119. package/plugins/opencode/skills/test-specification-alignment/references/specification-format.md +348 -0
  120. package/plugins/opencode/skills/to-issues/SKILL.md +277 -0
  121. package/plugins/opencode/tsconfig.json +15 -0
  122. package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +904 -0
  123. package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +45 -0
  124. package/plugins/qoder/skills/delphi-review/AGENTS.md +66 -0
  125. package/plugins/qoder/skills/delphi-review/INSTALL.md +152 -0
  126. package/plugins/qoder/skills/delphi-review/SKILL.md +454 -0
  127. package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +56 -0
  128. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +486 -0
  129. package/plugins/qoder/skills/improve-codebase-architecture/SKILL.md +156 -0
  130. package/plugins/qoder/skills/ralph-loop/SKILL.md +501 -0
  131. package/plugins/qoder/skills/ralph-loop/references/components/memory.md +55 -0
  132. package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +54 -0
  133. package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  134. package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +24 -0
  135. package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  136. package/plugins/qoder/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  137. package/plugins/qoder/skills/ralph-loop/templates/progress-log.md +36 -0
  138. package/plugins/qoder/skills/sprint-flow/AGENTS.md +68 -0
  139. package/plugins/qoder/skills/sprint-flow/SKILL.md +1248 -0
  140. package/plugins/qoder/skills/sprint-flow/references/components/memory.md +87 -0
  141. package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +72 -0
  142. package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  143. package/plugins/qoder/skills/sprint-flow/references/components/system-prompt.md +27 -0
  144. package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  145. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +203 -0
  146. package/plugins/qoder/skills/sprint-flow/references/phase-0-think.md +115 -0
  147. package/plugins/qoder/skills/sprint-flow/references/phase-1-plan.md +178 -0
  148. package/plugins/qoder/skills/sprint-flow/references/phase-2-build.md +198 -0
  149. package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +213 -0
  150. package/plugins/qoder/skills/sprint-flow/references/phase-4-uat.md +125 -0
  151. package/plugins/qoder/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  152. package/plugins/qoder/skills/sprint-flow/references/phase-6-ship.md +193 -0
  153. package/plugins/qoder/skills/sprint-flow/references/phase-7-land.md +140 -0
  154. package/plugins/qoder/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  155. package/plugins/qoder/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
  156. package/plugins/qoder/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
  157. package/plugins/qoder/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
  158. package/plugins/qoder/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  159. package/plugins/qoder/skills/sprint-flow/templates/pain-document-template.md +115 -0
  160. package/plugins/qoder/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
  161. package/plugins/qoder/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  162. package/plugins/qoder/skills/test-driven-development/SKILL.md +71 -0
  163. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +56 -0
  164. package/plugins/qoder/skills/test-specification-alignment/SKILL.md +702 -0
  165. package/plugins/qoder/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  166. package/plugins/qoder/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  167. package/plugins/qoder/skills/test-specification-alignment/references/specification-format.md +348 -0
  168. package/plugins/qoder/skills/to-issues/SKILL.md +277 -0
  169. package/skills/admin-template-guidelines/SKILL.md +904 -0
  170. package/skills/delphi-review/SKILL.md +112 -77
  171. package/skills/improve-codebase-architecture/SKILL.md +156 -0
  172. package/skills/ralph-loop/SKILL.md +165 -34
  173. package/skills/sprint-flow/SKILL.md +448 -107
  174. package/skills/sprint-flow/references/components/skill-invocations.md +1 -1
  175. package/skills/sprint-flow/references/components/tool-descriptions.md +1 -1
  176. package/skills/sprint-flow/references/force-levels.md +203 -0
  177. package/skills/sprint-flow/references/phase-1-plan.md +4 -4
  178. package/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +20 -1
  179. package/skills/sprint-flow/templates/auto-estimate-output-template.md +7 -5
  180. package/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
  181. package/skills/test-driven-development/SKILL.md +71 -0
  182. package/skills/test-specification-alignment/SKILL.md +98 -24
  183. package/skills/to-issues/SKILL.md +277 -0
  184. package/plugins/qoder/AGENTS.md +0 -93
  185. package/plugins/qoder/README.md +0 -87
  186. package/plugins/qoder/widgets/quality-report.html +0 -163
  187. package/plugins/qoder/widgets/sprint-dashboard.html +0 -172
  188. package/skills/delphi-review/evals/evals.json +0 -82
  189. package/skills/delphi-review/references/qoder-multi-model.md +0 -191
  190. package/skills/ralph-loop/evals/evals.json +0 -311
  191. package/skills/ralph-loop/evolution-history.json +0 -59
  192. package/skills/ralph-loop/evolution-log.md +0 -16
  193. package/skills/sprint-flow/evals/evals.json +0 -130
  194. package/skills/sprint-flow/evolution-history.json +0 -39
  195. package/skills/sprint-flow/evolution-log.md +0 -23
  196. package/skills/sprint-flow/references/qoder-adaptation.md +0 -173
  197. package/skills/test-specification-alignment/evals/evals.json +0 -75
@@ -3,29 +3,73 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
5
 
6
- /**
7
- * Resolve the user's home directory cross-platform.
8
- * Fallback chain: HOME → USERPROFILE → os.homedir()
9
- * This covers all known Windows/Linux/macOS/edge cases.
10
- */
11
- const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
12
-
13
- const SKILLS_DIR = path.join(HOME, '.config', 'opencode', 'skills');
14
- const OPENCODE_DIR = path.join(HOME, '.config', 'opencode');
6
+ const { HOME_DIR } = require('./shared-paths.js');
15
7
 
16
8
  const REQUIRED_DEPS = [
17
- { name: 'superpowers', minVersion: '1.0.0' },
18
- { name: 'gstack', minVersion: '1.0.0' }
9
+ { name: 'superpowers', repo: 'obra/superpowers', minVersion: '1.0.0' },
10
+ { name: 'gstack', repo: 'garrytan/gstack', minVersion: '1.0.0' }
19
11
  ];
20
12
 
21
- // Platform-specific dependency profiles
22
- // Qoder has no external skill dependencies (superpowers/gstack not required)
13
+ /**
14
+ * Platform profiles each AI agent platform has its own skills directory.
15
+ * All platforms require the same dependencies (superpowers, gstack).
16
+ *
17
+ * @covers Issue #128 Bug 1 — Qoder platform was excluded from dependency checks
18
+ */
23
19
  const PLATFORM_PROFILES = {
24
- opencode: { requiredDeps: REQUIRED_DEPS, skillsDirs: [SKILLS_DIR, OPENCODE_DIR] },
25
- qoder: { requiredDeps: [], skillsDirs: [path.join(HOME, '.qoder', 'skills')] },
26
- 'claude-code': { requiredDeps: REQUIRED_DEPS, skillsDirs: [SKILLS_DIR, OPENCODE_DIR] },
20
+ opencode: {
21
+ skillsDirs: [
22
+ path.join(HOME_DIR, '.config', 'opencode', 'skills'),
23
+ path.join(HOME_DIR, '.config', 'opencode'),
24
+ ],
25
+ requiredDeps: REQUIRED_DEPS,
26
+ },
27
+ 'claude-code': {
28
+ skillsDirs: [
29
+ path.join(HOME_DIR, '.claude', 'skills'),
30
+ path.join(HOME_DIR, '.claude'),
31
+ ],
32
+ requiredDeps: REQUIRED_DEPS,
33
+ },
34
+ qoder: {
35
+ skillsDirs: [
36
+ path.join(HOME_DIR, '.qoder', 'skills'),
37
+ path.join(HOME_DIR, '.qoder'),
38
+ ],
39
+ requiredDeps: REQUIRED_DEPS,
40
+ },
27
41
  };
28
42
 
43
+ /**
44
+ * Detect which AI agent platform is currently in use.
45
+ * Checks for platform-specific directories in the user's home.
46
+ * Falls back to 'opencode' if no platform is detected.
47
+ *
48
+ * @returns {'opencode' | 'claude-code' | 'qoder'}
49
+ */
50
+ function detectPlatform() {
51
+ // Check for Qoder-specific marker
52
+ if (fs.existsSync(path.join(HOME_DIR, '.qoder', 'skills'))) {
53
+ return 'qoder';
54
+ }
55
+ // Check for Claude Code-specific marker
56
+ if (fs.existsSync(path.join(HOME_DIR, '.claude', 'skills'))) {
57
+ return 'claude-code';
58
+ }
59
+ // Default to opencode (most common, backward compatible)
60
+ return 'opencode';
61
+ }
62
+
63
+ /**
64
+ * Get the skills directories for a given platform.
65
+ * @param {string} platform - 'opencode' | 'claude-code' | 'qoder'
66
+ * @returns {string[]}
67
+ */
68
+ function getSkillsDirs(platform) {
69
+ const profile = PLATFORM_PROFILES[platform] || PLATFORM_PROFILES.opencode;
70
+ return profile.skillsDirs;
71
+ }
72
+
29
73
  /**
30
74
  * Check if bash is available on the system.
31
75
  * XP-Gate hooks are bash scripts — Windows users need Git Bash installed.
@@ -92,30 +136,28 @@ function checkBash() {
92
136
  }
93
137
  }
94
138
 
139
+ /**
140
+ * Check if required dependencies are installed for the given platform.
141
+ * @param {string} [platform='opencode'] - AI agent platform
142
+ * @returns {Promise<{ok: boolean, missing?: string, versionMismatch?: object}>}
143
+ */
95
144
  async function checkDeps(platform = 'opencode') {
96
- const profile = PLATFORM_PROFILES[platform] || PLATFORM_PROFILES.opencode;
97
- const { requiredDeps, skillsDirs } = profile;
98
-
99
- // Qoder and other platforms with no hard dependencies pass immediately
100
- if (requiredDeps.length === 0) {
101
- return { ok: true, platform };
102
- }
145
+ const skillsDirs = getSkillsDirs(platform);
103
146
 
104
- for (const dep of requiredDeps) {
105
- const possiblePaths = skillsDirs.map(dir => path.join(dir, dep.name));
106
-
147
+ for (const dep of REQUIRED_DEPS) {
107
148
  let depDir = null;
108
- for (const p of possiblePaths) {
109
- if (fs.existsSync(p)) {
110
- depDir = p;
149
+ for (const baseDir of skillsDirs) {
150
+ const candidate = path.join(baseDir, dep.name);
151
+ if (fs.existsSync(candidate)) {
152
+ depDir = candidate;
111
153
  break;
112
154
  }
113
155
  }
114
-
156
+
115
157
  if (!depDir) {
116
158
  return { ok: false, missing: dep.name };
117
159
  }
118
-
160
+
119
161
  const version = await getSkillVersion(depDir);
120
162
  if (version && compareVersions(version, dep.minVersion) < 0) {
121
163
  return {
@@ -128,10 +170,103 @@ async function checkDeps(platform = 'opencode') {
128
170
  };
129
171
  }
130
172
  }
131
-
173
+
132
174
  return { ok: true };
133
175
  }
134
176
 
177
+ /**
178
+ * Auto-install missing dependencies by cloning from GitHub.
179
+ * @param {string} [platform='opencode'] - AI agent platform
180
+ * @returns {Promise<{ok: boolean, installed?: string[], errors?: Array<{name: string, message: string}>}>}
181
+ */
182
+ function runGitClone(repoUrl, destPath) {
183
+ const cp = require('child_process');
184
+ const result = cp.spawnSync(
185
+ 'git',
186
+ ['clone', '--depth', '1', repoUrl, destPath],
187
+ { stdio: ['ignore', 'pipe', 'pipe'], timeout: 120000, shell: false }
188
+ );
189
+ if (result.status === 0) return;
190
+ const stderr = String(result.stderr ?? '').trim();
191
+ throw new Error(stderr || `git clone exited with status ${result.status}`);
192
+ }
193
+
194
+ function ensureTargetDir(targetDir) {
195
+ if (fs.existsSync(targetDir)) return null;
196
+ try {
197
+ fs.mkdirSync(targetDir, { recursive: true });
198
+ return null;
199
+ } catch (e) {
200
+ return { name: 'mkdir', message: `Cannot create skills directory: ${e.message}` };
201
+ }
202
+ }
203
+
204
+ function versionMismatchError(versionMismatch) {
205
+ return {
206
+ name: versionMismatch.name,
207
+ message: `version mismatch: need ${versionMismatch.required}, found ${versionMismatch.found} (auto-install cannot upgrade)`,
208
+ };
209
+ }
210
+
211
+ function depExistsIn(skillsDirs, depName) {
212
+ for (const baseDir of skillsDirs) {
213
+ if (fs.existsSync(path.join(baseDir, depName))) return true;
214
+ }
215
+ return false;
216
+ }
217
+
218
+ function safeRemove(destPath) {
219
+ try {
220
+ if (fs.existsSync(destPath)) {
221
+ fs.rmSync(destPath, { recursive: true, force: true });
222
+ }
223
+ } catch {
224
+ // best-effort cleanup of partial clone
225
+ }
226
+ }
227
+
228
+ function installOneDep(dep, targetDir) {
229
+ const destPath = path.join(targetDir, dep.name);
230
+ const repoUrl = `https://github.com/${dep.repo}.git`;
231
+ try {
232
+ console.log(` ${dep.name}: not found → installing from ${repoUrl} ...`);
233
+ runGitClone(repoUrl, destPath);
234
+ console.log(` ${dep.name}: OK`);
235
+ return { ok: true };
236
+ } catch (e) {
237
+ const message = e.message || 'git clone failed';
238
+ console.warn(` ${dep.name}: FAILED (${message})`);
239
+ safeRemove(destPath);
240
+ return { ok: false, error: { name: dep.name, message } };
241
+ }
242
+ }
243
+
244
+ async function autoInstallDeps(platform = 'opencode') {
245
+ const skillsDirs = getSkillsDirs(platform);
246
+ const targetDir = skillsDirs[0];
247
+
248
+ const mkdirErr = ensureTargetDir(targetDir);
249
+ if (mkdirErr) return { ok: false, errors: [mkdirErr] };
250
+
251
+ const depCheck = await checkDeps(platform);
252
+ if (depCheck.ok) return { ok: true, installed: [] };
253
+ if (depCheck.versionMismatch) {
254
+ return { ok: false, installed: [], errors: [versionMismatchError(depCheck.versionMismatch)] };
255
+ }
256
+
257
+ const errors = [];
258
+ const installed = [];
259
+ for (const dep of REQUIRED_DEPS) {
260
+ if (depExistsIn(skillsDirs, dep.name)) continue;
261
+ const result = installOneDep(dep, targetDir);
262
+ if (result.ok) installed.push(dep.name);
263
+ else errors.push(result.error);
264
+ }
265
+
266
+ if (errors.length > 0) return { ok: false, installed, errors };
267
+ return { ok: true, installed };
268
+ }
269
+
135
270
  async function getSkillVersion(skillDir) {
136
271
  const pkgFile = path.join(skillDir, 'package.json');
137
272
  if (fs.existsSync(pkgFile)) {
@@ -140,7 +275,7 @@ async function getSkillVersion(skillDir) {
140
275
  return pkg.version;
141
276
  } catch {}
142
277
  }
143
-
278
+
144
279
  const skillFile = path.join(skillDir, 'SKILL.md');
145
280
  if (fs.existsSync(skillFile)) {
146
281
  const content = fs.readFileSync(skillFile, 'utf8');
@@ -149,22 +284,30 @@ async function getSkillVersion(skillDir) {
149
284
  return versionMatch[1];
150
285
  }
151
286
  }
152
-
287
+
153
288
  return null;
154
289
  }
155
290
 
156
291
  function compareVersions(a, b) {
157
292
  const partsA = a.split('.').map(Number);
158
293
  const partsB = b.split('.').map(Number);
159
-
294
+
160
295
  for (let i = 0; i < 3; i++) {
161
296
  const partA = partsA[i] || 0;
162
297
  const partB = partsB[i] || 0;
163
298
  if (partA > partB) return 1;
164
299
  if (partA < partB) return -1;
165
300
  }
166
-
301
+
167
302
  return 0;
168
303
  }
169
304
 
170
- module.exports = { checkDeps, checkBash };
305
+ module.exports = {
306
+ checkDeps,
307
+ checkBash,
308
+ autoInstallDeps,
309
+ detectPlatform,
310
+ getSkillsDirs,
311
+ PLATFORM_PROFILES,
312
+ REQUIRED_DEPS,
313
+ };
package/lib/doctor.js CHANGED
@@ -1,15 +1,13 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const { execSync } = require('child_process');
5
-
6
- // Cross-platform home directory resolution
7
- const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
8
-
9
- const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
10
- const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
11
- const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
12
- const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
4
+ const {
5
+ HOME_DIR,
6
+ CONFIG_DIR,
7
+ CONFIG_FILE,
8
+ GLOBAL_HOOKS_DIR,
9
+ GLOBAL_ADAPTERS_DIR,
10
+ } = require('./shared-paths.js');
13
11
 
14
12
  // npm package source dir (template hooks/adapters)
15
13
  const PKG_DIR = path.dirname(__dirname);
package/lib/gate-audit.ts CHANGED
@@ -177,64 +177,44 @@ export function computeStats(
177
177
  * Renames current file to .1, shifts existing archives.
178
178
  * Keeps at most MAX_ARCHIVES archived files.
179
179
  */
180
- export function rotateIfNeeded(logPath: string): void {
181
- if (!existsSync(logPath)) {
182
- return;
183
- }
184
-
185
- let stats;
180
+ function shouldRotate(logPath: string): boolean {
181
+ if (!existsSync(logPath)) return false;
186
182
  try {
187
- stats = statSync(logPath);
183
+ return statSync(logPath).size >= MAX_FILE_BYTES;
188
184
  } catch {
189
- return;
190
- }
191
-
192
- if (stats.size < MAX_FILE_BYTES) {
193
- return;
185
+ return false;
194
186
  }
187
+ }
195
188
 
196
- // Shift archives: .2 -> .3, .1 -> .2
189
+ function shiftOldArchives(logPath: string): void {
197
190
  for (let i = MAX_ARCHIVES; i >= 2; i--) {
198
191
  const from = `${logPath}.${i - 1}`;
199
192
  const to = `${logPath}.${i}`;
200
193
  if (existsSync(from)) {
201
- try {
202
- renameSync(from, to);
203
- } catch {
204
- // Race condition or permission issue — skip
205
- }
194
+ try { renameSync(from, to); } catch { /* skip */ }
206
195
  }
207
196
  }
197
+ }
208
198
 
209
- // Current -> .1
210
- const firstArchive = `${logPath}.1`;
211
- if (existsSync(firstArchive)) {
212
- try {
213
- renameSync(firstArchive, `${logPath}.2`);
214
- } catch {
215
- // Skip on error
216
- }
199
+ function archiveCurrent(logPath: string): void {
200
+ const first = `${logPath}.1`;
201
+ if (existsSync(first)) {
202
+ try { renameSync(first, `${logPath}.2`); } catch { /* skip */ }
217
203
  }
218
-
219
204
  try {
220
- renameSync(logPath, firstArchive);
205
+ renameSync(logPath, first);
221
206
  } catch {
222
- // If rename fails, truncate the current file instead
223
- try {
224
- writeFileSync(logPath, '', 'utf8');
225
- } catch {
226
- console.error('[xp-gate audit] Failed to rotate or truncate log:', logPath);
227
- }
207
+ try { writeFileSync(logPath, '', 'utf8'); } catch { /* skip */ }
228
208
  }
229
209
  }
230
210
 
231
- // ── CLI entry point (direct invocation from hooks) ───────────────────────────
211
+ export function rotateIfNeeded(logPath: string): void {
212
+ if (!shouldRotate(logPath)) return;
213
+ shiftOldArchives(logPath);
214
+ archiveCurrent(logPath);
215
+ }
232
216
 
233
- /**
234
- * When called directly via `npx tsx gate-audit.ts record --gate-id ...`,
235
- * this block handles CLI argument parsing and appends the entry.
236
- */
237
- if (require.main === module) {
217
+ function runCli(): void {
238
218
  const args = process.argv.slice(2);
239
219
  const command = args[0];
240
220
 
@@ -258,7 +238,11 @@ if (require.main === module) {
258
238
  }
259
239
  }
260
240
 
261
- function parseCliOptions(args: string[]): Record<string, string> {
241
+ if (require.main === module) {
242
+ runCli();
243
+ }
244
+
245
+ export function parseCliOptions(args: string[]): Record<string, string> {
262
246
  const opts: Record<string, string> = {};
263
247
  for (let i = 0; i < args.length; i++) {
264
248
  if (args[i].startsWith('--') && i + 1 < args.length) {
@@ -269,7 +253,7 @@ function parseCliOptions(args: string[]): Record<string, string> {
269
253
  return opts;
270
254
  }
271
255
 
272
- function getCommitHash(): string {
256
+ export function getCommitHash(): string {
273
257
  try {
274
258
  const { execSync } = require('child_process');
275
259
  return execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
package/lib/init.js CHANGED
@@ -1,18 +1,15 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const crypto = require('crypto');
5
- const { checkDeps } = require('./detect-deps.js');
6
- const { checkBash } = require('./detect-deps.js');
7
-
8
- // Cross-platform home directory resolution with os.homedir() fallback
9
- const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
10
-
11
- const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
12
- const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
13
- const TEMPLATE_DIR = path.join(HOME_DIR, '.config', 'opencode', 'git-hooks-template');
14
- const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
15
- const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
4
+ const { checkDeps, checkBash, autoInstallDeps, detectPlatform } = require('./detect-deps.js');
5
+ const {
6
+ HOME_DIR,
7
+ CONFIG_DIR,
8
+ CONFIG_FILE,
9
+ TEMPLATE_DIR,
10
+ GLOBAL_HOOKS_DIR,
11
+ GLOBAL_ADAPTERS_DIR,
12
+ } = require('./shared-paths.js');
16
13
 
17
14
  function copyHooks(srcDir, destDir) {
18
15
  ['pre-commit', 'pre-push'].forEach(hook => {
@@ -54,6 +51,35 @@ function logDeps(depCheck) {
54
51
  }
55
52
  }
56
53
 
54
+ /**
55
+ * Detect which AI agent platform is in use and check/auto-install deps.
56
+ * @param {string} platform - 'opencode' | 'claude-code' | 'qoder'
57
+ */
58
+ async function checkAndInstallDeps(platform) {
59
+ const depCheck = await checkDeps(platform);
60
+ if (depCheck.ok) {
61
+ logDeps(depCheck);
62
+ return depCheck;
63
+ }
64
+
65
+ // Auto-install missing deps
66
+ console.log(`Checking dependencies (platform: ${platform})...`);
67
+ const installResult = await autoInstallDeps(platform);
68
+ if (installResult.ok) {
69
+ console.log('Dependencies: OK (auto-installed)\n');
70
+ return { ok: true };
71
+ }
72
+
73
+ // Auto-install failed — report and continue with warning
74
+ logDeps(depCheck);
75
+ if (installResult.errors) {
76
+ for (const err of installResult.errors) {
77
+ console.warn(` Auto-install failed for ${err.name}: ${err.message}`);
78
+ }
79
+ }
80
+ return depCheck;
81
+ }
82
+
57
83
  function printUsage() {
58
84
  console.log('Choose installation mode:');
59
85
  console.log(' 1) Global — all git projects use the same hooks (recommended)');
@@ -228,7 +254,10 @@ async function init(args) {
228
254
  console.warn(` ${bashCheck.message}\n`);
229
255
  }
230
256
 
231
- logDeps(await checkDeps());
257
+ // Detect platform and check/auto-install dependencies
258
+ const platform = detectPlatform();
259
+ console.log(`Platform: ${platform}\n`);
260
+ await checkAndInstallDeps(platform);
232
261
 
233
262
  const installMode = args.includes('--global') ? 'global' :
234
263
  args.includes('--core-only') ? 'local' :
@@ -1,25 +1,14 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const https = require('https');
5
4
  const http = require('http');
6
5
  const { execSync } = require('child_process');
7
- const { checkDeps } = require('./detect-deps.js');
6
+ const { checkDeps, detectPlatform } = require('./detect-deps.js');
8
7
  const { downloadFromGitHub } = require('./download-skill.js');
9
8
  const { rollback } = require('./rollback.js');
9
+ const { HOME_DIR, CONFIG_DIR } = require('./shared-paths.js');
10
10
 
11
- // Cross-platform home directory resolution
12
- const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
13
-
14
- const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
15
- const SKILLS_DIR = path.join(HOME, '.config', 'opencode', 'skills');
16
- const QODER_SKILLS_DIR = path.join(HOME, '.qoder', 'skills');
17
-
18
- // Platform-specific skill directories
19
- const PLATFORM_SKILLS_DIRS = {
20
- opencode: SKILLS_DIR,
21
- qoder: QODER_SKILLS_DIR,
22
- };
11
+ const SKILLS_DIR = path.join(HOME_DIR, '.config', 'opencode', 'skills');
23
12
 
24
13
  const SKILLS_REGISTRY = {
25
14
  'sprint-flow': { repo: 'boyingliu01/xp-gate', path: 'skills/sprint-flow' },
@@ -29,23 +18,21 @@ const SKILLS_REGISTRY = {
29
18
  };
30
19
 
31
20
  async function installSkill(name, options = {}) {
32
- const { offline = false, verbose = false, force = false, platform = 'opencode' } = options;
33
-
34
- // Qoder platform has no superpowers/gstack dependency
35
- if (platform !== 'qoder') {
36
- const depCheck = await checkDeps();
37
- if (!depCheck.ok) {
38
- if (depCheck.missing) {
39
- console.error(`Error: ${depCheck.missing} is required but not installed`);
40
- console.error('Please install superpowers and gstack first');
41
- console.error('See: https://github.com/boyingliu01/superpowers');
42
- return 1;
43
- }
44
- if (depCheck.versionMismatch) {
45
- console.error(`Error: ${depCheck.versionMismatch.name} version too old`);
46
- console.error(`Need: ${depCheck.versionMismatch.required}, Found: ${depCheck.versionMismatch.found}`);
47
- return 1;
48
- }
21
+ const { offline = false, verbose = false, force = false } = options;
22
+
23
+ const platform = detectPlatform();
24
+ const depCheck = await checkDeps(platform);
25
+ if (!depCheck.ok) {
26
+ if (depCheck.missing) {
27
+ console.error(`Error: ${depCheck.missing} is required but not installed`);
28
+ console.error('Please install superpowers and gstack first');
29
+ console.error('See: https://github.com/boyingliu01/superpowers');
30
+ return 1;
31
+ }
32
+ if (depCheck.versionMismatch) {
33
+ console.error(`Error: ${depCheck.versionMismatch.name} version too old`);
34
+ console.error(`Need: ${depCheck.versionMismatch.required}, Found: ${depCheck.versionMismatch.found}`);
35
+ return 1;
49
36
  }
50
37
  }
51
38
 
@@ -56,8 +43,7 @@ async function installSkill(name, options = {}) {
56
43
  return 1;
57
44
  }
58
45
 
59
- const targetSkillsDir = PLATFORM_SKILLS_DIRS[platform] || SKILLS_DIR;
60
- const targetDir = path.join(targetSkillsDir, name);
46
+ const targetDir = path.join(SKILLS_DIR, name);
61
47
  if (fs.existsSync(targetDir) && !force) {
62
48
  console.error(`Error: ${name} is already installed`);
63
49
  console.error('Use --force to overwrite');
@@ -155,21 +141,7 @@ async function downloadFile(url, dest, verbose) {
155
141
  });
156
142
  }
157
143
 
158
- function copyDirRecursive(src, dest) {
159
- fs.mkdirSync(dest, { recursive: true });
160
- const entries = fs.readdirSync(src, { withFileTypes: true });
161
-
162
- for (const entry of entries) {
163
- const srcPath = path.join(src, entry.name);
164
- const destPath = path.join(dest, entry.name);
165
-
166
- if (entry.isDirectory()) {
167
- copyDirRecursive(srcPath, destPath);
168
- } else {
169
- fs.copyFileSync(srcPath, destPath);
170
- }
171
- }
172
- }
144
+ const { copyDirRecursive } = require('./shared-utils');
173
145
 
174
146
  function ensureConfigDir() {
175
147
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
package/lib/rollback.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const { copyDirRecursive } = require('./shared-utils');
4
5
 
5
6
  // Cross-platform home directory resolution
6
7
  const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -51,22 +52,6 @@ async function createBackup(installId, skillName) {
51
52
  return backupDir;
52
53
  }
53
54
 
54
- function copyDirRecursive(src, dest) {
55
- fs.mkdirSync(dest, { recursive: true });
56
- const entries = fs.readdirSync(src, { withFileTypes: true });
57
-
58
- for (const entry of entries) {
59
- const srcPath = path.join(src, entry.name);
60
- const destPath = path.join(dest, entry.name);
61
-
62
- if (entry.isDirectory()) {
63
- copyDirRecursive(srcPath, destPath);
64
- } else {
65
- fs.copyFileSync(srcPath, destPath);
66
- }
67
- }
68
- }
69
-
70
55
  function cleanupBackup(installId) {
71
56
  const backupDir = path.join(BACKUP_DIR, installId);
72
57
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared filesystem path constants used across CLI modules.
3
+ * Cross-platform home directory resolution with os.homedir() fallback.
4
+ *
5
+ * @intent Eliminate duplicate path constants in init.js / uninstall.js / detect-deps.js
6
+ * @covers Issue #107 — duplicate code between init.js and uninstall.js
7
+ */
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ /**
12
+ * Resolve the user's home directory cross-platform.
13
+ * Fallback chain: HOME → USERPROFILE → os.homedir()
14
+ */
15
+ const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
16
+
17
+ const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
18
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
19
+ const TEMPLATE_DIR = path.join(HOME_DIR, '.config', 'opencode', 'git-hooks-template');
20
+ const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
21
+ const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
22
+
23
+ module.exports = {
24
+ HOME_DIR,
25
+ CONFIG_DIR,
26
+ CONFIG_FILE,
27
+ TEMPLATE_DIR,
28
+ GLOBAL_HOOKS_DIR,
29
+ GLOBAL_ADAPTERS_DIR,
30
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure utility functions shared by CLI modules.
3
+ * No module-level state — safe for tests that mock fs/path/os.
4
+ */
5
+ const fs = require('fs');
6
+
7
+ /**
8
+ * Recursively copy a directory.
9
+ * Pure function: only uses fs/path params, no global config.
10
+ */
11
+ function copyDirRecursive(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true });
13
+ const entries = fs.readdirSync(src, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const srcPath = require('path').join(src, entry.name);
16
+ const destPath = require('path').join(dest, entry.name);
17
+ if (entry.isDirectory()) {
18
+ copyDirRecursive(srcPath, destPath);
19
+ } else {
20
+ fs.copyFileSync(srcPath, destPath);
21
+ }
22
+ }
23
+ }
24
+
25
+ module.exports = { copyDirRecursive };