@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
package/bin/xp-gate.js CHANGED
@@ -22,7 +22,7 @@ const COMMANDS = {
22
22
  'install-skill': {
23
23
  description: 'Install a xp-gate skill from GitHub',
24
24
  fn: installSkill,
25
- usage: 'xp-gate install-skill <name>[@<version>] [--offline] [--verbose] [--force] [--platform opencode|qoder]'
25
+ usage: 'xp-gate install-skill <name>[@<version>] [--offline] [--verbose] [--force]'
26
26
  },
27
27
  'update-skill': {
28
28
  description: 'Update installed skill(s)',
@@ -103,7 +103,7 @@ function main() {
103
103
  const name = subargs[0];
104
104
  if (!name) {
105
105
  console.error('Error: Skill name required');
106
- console.error('Usage: xp-gate install-skill <name>[@<version>] [--platform opencode|qoder]');
106
+ console.error('Usage: xp-gate install-skill <name>[@<version>]');
107
107
  process.exit(1);
108
108
  return;
109
109
  }
@@ -285,18 +285,13 @@ function printStatsTable(stats) {
285
285
  }
286
286
 
287
287
  function parseOptions(args) {
288
- const options = { offline: false, verbose: false, force: false, all: false, check: false, platform: 'opencode' };
289
- for (let i = 0; i < args.length; i++) {
290
- const arg = args[i];
288
+ const options = { offline: false, verbose: false, force: false, all: false, check: false };
289
+ for (const arg of args) {
291
290
  if (arg === '--offline') options.offline = true;
292
291
  if (arg === '--verbose') options.verbose = true;
293
292
  if (arg === '--force') options.force = true;
294
293
  if (arg === '--all') options.all = true;
295
294
  if (arg === '--check') options.check = true;
296
- if (arg === '--platform' && i + 1 < args.length) {
297
- options.platform = args[i + 1];
298
- i++;
299
- }
300
295
  }
301
296
  return options;
302
297
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @test audit-log
3
+ * @intent Verify UI gate bypass audit logging and threshold reporting
4
+ * @covers AUDIT-LOG-001
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import {
10
+ appendAuditEntry,
11
+ readAllEntries,
12
+ countBypassesInWindow,
13
+ exceedsBypassThreshold,
14
+ formatRetroReport,
15
+ type AuditEntry,
16
+ } from '../audit-log';
17
+
18
+ const TEST_DIR = join(process.cwd(), '.audit-log-test');
19
+
20
+ function baseEntry(overrides: Partial<Omit<AuditEntry, 'gate_count'>> = {}): Omit<AuditEntry, 'gate_count'> {
21
+ return {
22
+ timestamp: new Date().toISOString(),
23
+ branch: 'main',
24
+ commit: 'abc123',
25
+ user: 'tester',
26
+ reason: 'manual approval for test',
27
+ bypass_type: 'ui-gates',
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ describe('audit-log', () => {
33
+ beforeEach(() => {
34
+ if (existsSync(TEST_DIR)) {
35
+ rmSync(TEST_DIR, { recursive: true, force: true });
36
+ }
37
+ mkdirSync(TEST_DIR, { recursive: true });
38
+ });
39
+
40
+ afterEach(() => {
41
+ if (existsSync(TEST_DIR)) {
42
+ rmSync(TEST_DIR, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ describe('appendAuditEntry and readAllEntries', () => {
47
+ it('should append an entry and compute gate_count', () => {
48
+ const first = appendAuditEntry(baseEntry(), TEST_DIR);
49
+ const second = appendAuditEntry(baseEntry({ commit: 'def456' }), TEST_DIR);
50
+
51
+ expect(first.gate_count).toBe(0);
52
+ expect(second.gate_count).toBe(1);
53
+
54
+ const entries = readAllEntries(join(TEST_DIR, '.audit-log.jsonl'));
55
+ expect(entries).toHaveLength(2);
56
+ expect(entries[1].commit).toBe('def456');
57
+ });
58
+
59
+ it('should return empty array when log file is missing or empty', () => {
60
+ expect(readAllEntries(join(TEST_DIR, 'missing.jsonl'))).toEqual([]);
61
+ const emptyLog = join(TEST_DIR, '.audit-log.jsonl');
62
+ writeFileSync(emptyLog, '', 'utf8');
63
+ expect(readAllEntries(emptyLog)).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe('rolling window calculations', () => {
68
+ it('should count only matching bypass type in rolling window', () => {
69
+ const now = new Date();
70
+ const old = new Date(now.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString();
71
+ const entries: AuditEntry[] = [
72
+ { ...baseEntry({ timestamp: now.toISOString(), bypass_type: 'ui-gates' }), gate_count: 0 },
73
+ { ...baseEntry({ timestamp: now.toISOString(), bypass_type: 'other' }), gate_count: 0 },
74
+ { ...baseEntry({ timestamp: old, bypass_type: 'ui-gates' }), gate_count: 0 },
75
+ ];
76
+
77
+ expect(countBypassesInWindow(entries, 'ui-gates')).toBe(1);
78
+ });
79
+
80
+ it('should report threshold exceedance', () => {
81
+ const entries: AuditEntry[] = Array.from({ length: 4 }, (_, i) => ({
82
+ ...baseEntry({ commit: `commit-${i}` }),
83
+ gate_count: i,
84
+ }));
85
+ expect(exceedsBypassThreshold(entries, 'ui-gates')).toEqual({ exceeded: true, count: 4 });
86
+ });
87
+ });
88
+
89
+ describe('formatRetroReport', () => {
90
+ it('should include recent bypasses and threshold warning', () => {
91
+ const entries: AuditEntry[] = Array.from({ length: 4 }, (_, i) => ({
92
+ ...baseEntry({ reason: `reason-${i}`, branch: `branch-${i}` }),
93
+ gate_count: i,
94
+ }));
95
+ const report = formatRetroReport(entries, 'ui-gates');
96
+ expect(report).toContain('Last 30 days: 4 bypasses');
97
+ expect(report).toContain('EXCEEDS threshold');
98
+ expect(report).toContain('reason-3');
99
+ });
100
+
101
+ it('should omit recent section when no matching bypasses exist', () => {
102
+ const report = formatRetroReport([], 'ui-gates');
103
+ expect(report).toContain('Last 30 days: 0 bypasses');
104
+ expect(report).not.toContain('Recent bypasses');
105
+ });
106
+ });
107
+ });
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @test detect-deps
3
- * @intent Verify checkDeps() correctly detects missing/present/outdated dependencies
3
+ * @intent Verify checkDeps() correctly detects missing/present/outdated dependencies across platforms,
4
+ * detectPlatform() identifies the correct AI agent platform,
5
+ * and autoInstallDeps() handles install scenarios
4
6
  */
5
7
  const fs = require('fs');
6
8
  const path = require('path');
@@ -16,6 +18,7 @@ describe('detect-deps', () => {
16
18
  process.env.HOME = tmpHome;
17
19
  vi.resetModules();
18
20
  delete require.cache[require.resolve('../detect-deps')];
21
+ delete require.cache[require.resolve('../shared-paths')];
19
22
  });
20
23
 
21
24
  afterEach(() => {
@@ -24,8 +27,10 @@ describe('detect-deps', () => {
24
27
  vi.restoreAllMocks();
25
28
  });
26
29
 
27
- function makeSkillDir(skillName, contents = {}) {
28
- const dir = path.join(tmpHome, '.config', 'opencode', 'skills', skillName);
30
+ function makeSkillDir(skillName, contents = {}, baseDir) {
31
+ const dir = baseDir
32
+ ? path.join(baseDir, skillName)
33
+ : path.join(tmpHome, '.config', 'opencode', 'skills', skillName);
29
34
  fs.mkdirSync(dir, { recursive: true });
30
35
  if (contents.packageJson) {
31
36
  fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(contents.packageJson));
@@ -48,6 +53,8 @@ describe('detect-deps', () => {
48
53
  return dir;
49
54
  }
50
55
 
56
+ // ── checkDeps: backward-compatible tests (default platform = opencode) ──
57
+
51
58
  it('returns ok:false missing:superpowers when no deps exist', async () => {
52
59
  const { checkDeps } = require('../detect-deps');
53
60
  const result = await checkDeps();
@@ -127,7 +134,6 @@ describe('detect-deps', () => {
127
134
  });
128
135
 
129
136
  it('returns ok:true (no version check) when package.json has no version and no SKILL.md', async () => {
130
- // getSkillVersion returns null → skips version check → ok
131
137
  makeSkillDir('superpowers', { packageJson: {} });
132
138
  makeSkillDir('gstack', { packageJson: {} });
133
139
  const { checkDeps } = require('../detect-deps');
@@ -147,13 +153,8 @@ describe('detect-deps', () => {
147
153
  });
148
154
 
149
155
  it('returns null version when neither package.json nor SKILL.md exist (skips version check)', async () => {
150
- // Create empty dirs (no metadata files)
151
- fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'superpowers'), {
152
- recursive: true,
153
- });
154
- fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'gstack'), {
155
- recursive: true,
156
- });
156
+ fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'superpowers'), { recursive: true });
157
+ fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'gstack'), { recursive: true });
157
158
  const { checkDeps } = require('../detect-deps');
158
159
  const result = await checkDeps();
159
160
  expect(result.ok).toBe(true);
@@ -164,17 +165,14 @@ describe('detect-deps', () => {
164
165
  makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
165
166
  const { checkDeps } = require('../detect-deps');
166
167
  const result = await checkDeps();
167
- // superpowers version=null → skips version check → ok
168
168
  expect(result.ok).toBe(true);
169
169
  });
170
170
 
171
- it('compareVersions handles partial versions (e.g. 1.0 treated as 1.0.0)', async () => {
172
- // version "1.0" in SKILL.md regex requires X.Y.Z so won't match; use package.json
171
+ it('compareVersions handles partial versions (e.g. 1 treated as 1.0.0)', async () => {
173
172
  makeSkillDir('superpowers', { packageJson: { version: '1' } });
174
173
  makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
175
174
  const { checkDeps } = require('../detect-deps');
176
175
  const result = await checkDeps();
177
- // '1' parsed as [1] vs [1,0,0] → equal at index 0; index 1: 0 vs 0; index 2: 0 vs 0 → equal → passes
178
176
  expect(result.ok).toBe(true);
179
177
  });
180
178
 
@@ -196,48 +194,220 @@ describe('detect-deps', () => {
196
194
  });
197
195
 
198
196
  it('prefers SKILLS_DIR over OPENCODE_DIR when both exist', async () => {
199
- // Put low version in SKILLS_DIR, high in OPENCODE_DIR
200
197
  makeSkillDir('superpowers', { packageJson: { version: '0.0.1' } });
201
198
  makeOpencodeDir('superpowers', { packageJson: { version: '2.0.0' } });
202
199
  makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
203
200
  const { checkDeps } = require('../detect-deps');
204
201
  const result = await checkDeps();
205
- // Should use SKILLS_DIR (first in possiblePaths) → 0.0.1 fails
206
202
  expect(result.ok).toBe(false);
207
203
  expect(result.versionMismatch.found).toBe('0.0.1');
208
204
  });
209
205
 
210
- // --- Qoder platform tests ---
206
+ // ── Platform-specific tests (Issue #128) ──
211
207
 
212
- it('returns ok:true immediately for qoder platform (no hard deps)', async () => {
213
- // Qoder has no superpowers/gstack dependency should pass even with empty dirs
214
- const { checkDeps } = require('../detect-deps');
215
- const result = await checkDeps('qoder');
216
- expect(result.ok).toBe(true);
217
- expect(result.platform).toBe('qoder');
208
+ describe('checkDeps with platform parameter', () => {
209
+ it('qoder: checks ~/.qoder/skills/ for dependencies', async () => {
210
+ const qoderSkills = path.join(tmpHome, '.qoder', 'skills');
211
+ makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } }, path.join(qoderSkills, 'superpowers'));
212
+ makeSkillDir('gstack', { packageJson: { version: '1.0.0' } }, path.join(qoderSkills, 'gstack'));
213
+ const { checkDeps } = require('../detect-deps');
214
+ const result = await checkDeps('qoder');
215
+ expect(result.ok).toBe(true);
216
+ });
217
+
218
+ it('qoder: returns missing when qoder skills dir is empty', async () => {
219
+ fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
220
+ const { checkDeps } = require('../detect-deps');
221
+ const result = await checkDeps('qoder');
222
+ expect(result.ok).toBe(false);
223
+ expect(result.missing).toBe('superpowers');
224
+ });
225
+
226
+ it('claude-code: checks ~/.claude/skills/ for dependencies', async () => {
227
+ const claudeSkills = path.join(tmpHome, '.claude', 'skills');
228
+ makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } }, path.join(claudeSkills, 'superpowers'));
229
+ makeSkillDir('gstack', { packageJson: { version: '1.0.0' } }, path.join(claudeSkills, 'gstack'));
230
+ const { checkDeps } = require('../detect-deps');
231
+ const result = await checkDeps('claude-code');
232
+ expect(result.ok).toBe(true);
233
+ });
234
+
235
+ it('claude-code: returns missing when claude skills dir is empty', async () => {
236
+ fs.mkdirSync(path.join(tmpHome, '.claude', 'skills'), { recursive: true });
237
+ const { checkDeps } = require('../detect-deps');
238
+ const result = await checkDeps('claude-code');
239
+ expect(result.ok).toBe(false);
240
+ expect(result.missing).toBe('superpowers');
241
+ });
242
+
243
+ it('unknown platform falls back to opencode profile', async () => {
244
+ makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
245
+ makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
246
+ const { checkDeps } = require('../detect-deps');
247
+ const result = await checkDeps('unknown-platform');
248
+ expect(result.ok).toBe(true);
249
+ });
250
+
251
+ it('qoder platform does NOT find deps in opencode dir', async () => {
252
+ // Put deps in opencode dir but NOT in qoder dir
253
+ makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
254
+ makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
255
+ const { checkDeps } = require('../detect-deps');
256
+ const result = await checkDeps('qoder');
257
+ expect(result.ok).toBe(false);
258
+ expect(result.missing).toBe('superpowers');
259
+ });
218
260
  });
219
261
 
220
- it('qoder platform passes even when superpowers/gstack are completely absent', async () => {
221
- // No skill directories at all — qoder should still pass
222
- const { checkDeps } = require('../detect-deps');
223
- const result = await checkDeps('qoder');
224
- expect(result.ok).toBe(true);
225
- expect(result.missing).toBeUndefined();
262
+ // ── detectPlatform tests ──
263
+
264
+ describe('detectPlatform', () => {
265
+ it('returns "qoder" when ~/.qoder/skills/ exists', () => {
266
+ fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
267
+ const { detectPlatform } = require('../detect-deps');
268
+ expect(detectPlatform()).toBe('qoder');
269
+ });
270
+
271
+ it('returns "claude-code" when ~/.claude/skills/ exists', () => {
272
+ fs.mkdirSync(path.join(tmpHome, '.claude', 'skills'), { recursive: true });
273
+ const { detectPlatform } = require('../detect-deps');
274
+ expect(detectPlatform()).toBe('claude-code');
275
+ });
276
+
277
+ it('returns "opencode" as default when no platform dirs exist', () => {
278
+ const { detectPlatform } = require('../detect-deps');
279
+ expect(detectPlatform()).toBe('opencode');
280
+ });
281
+
282
+ it('prefers qoder over claude-code when both exist', () => {
283
+ fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
284
+ fs.mkdirSync(path.join(tmpHome, '.claude', 'skills'), { recursive: true });
285
+ const { detectPlatform } = require('../detect-deps');
286
+ expect(detectPlatform()).toBe('qoder');
287
+ });
226
288
  });
227
289
 
228
- it('claude-code platform behaves like default opencode (requires deps)', async () => {
229
- // claude-code platform profile requires the same deps as opencode
230
- const { checkDeps } = require('../detect-deps');
231
- const result = await checkDeps('claude-code');
232
- expect(result.ok).toBe(false);
233
- expect(result.missing).toBe('superpowers');
290
+ // ── getSkillsDirs tests ──
291
+
292
+ describe('getSkillsDirs', () => {
293
+ it('returns opencode paths for opencode platform', () => {
294
+ const { getSkillsDirs } = require('../detect-deps');
295
+ const dirs = getSkillsDirs('opencode');
296
+ expect(dirs[0]).toContain('.config');
297
+ expect(dirs[0]).toContain('opencode');
298
+ });
299
+
300
+ it('returns qoder paths for qoder platform', () => {
301
+ const { getSkillsDirs } = require('../detect-deps');
302
+ const dirs = getSkillsDirs('qoder');
303
+ expect(dirs[0]).toContain('.qoder');
304
+ });
305
+
306
+ it('returns claude-code paths for claude-code platform', () => {
307
+ const { getSkillsDirs } = require('../detect-deps');
308
+ const dirs = getSkillsDirs('claude-code');
309
+ expect(dirs[0]).toContain('.claude');
310
+ });
234
311
  });
235
312
 
236
- it('unknown platform falls back to opencode profile', async () => {
237
- const { checkDeps } = require('../detect-deps');
238
- const result = await checkDeps('unknown-platform');
239
- // Falls back to opencode profile requires deps → fails
240
- expect(result.ok).toBe(false);
241
- expect(result.missing).toBe('superpowers');
313
+ // ── PLATFORM_PROFILES tests ──
314
+
315
+ describe('PLATFORM_PROFILES', () => {
316
+ it('all platforms have requiredDeps defined', () => {
317
+ const { PLATFORM_PROFILES } = require('../detect-deps');
318
+ expect(PLATFORM_PROFILES.opencode.requiredDeps.length).toBe(2);
319
+ expect(PLATFORM_PROFILES.qoder.requiredDeps.length).toBe(2);
320
+ expect(PLATFORM_PROFILES['claude-code'].requiredDeps.length).toBe(2);
321
+ });
322
+
323
+ it('all platforms have skillsDirs defined', () => {
324
+ const { PLATFORM_PROFILES } = require('../detect-deps');
325
+ expect(PLATFORM_PROFILES.opencode.skillsDirs.length).toBeGreaterThan(0);
326
+ expect(PLATFORM_PROFILES.qoder.skillsDirs.length).toBeGreaterThan(0);
327
+ expect(PLATFORM_PROFILES['claude-code'].skillsDirs.length).toBeGreaterThan(0);
328
+ });
329
+ });
330
+
331
+ // ── autoInstallDeps tests ──
332
+
333
+ describe('autoInstallDeps', () => {
334
+ it('returns ok:true with empty installed when deps already exist', async () => {
335
+ makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
336
+ makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
337
+ const { autoInstallDeps } = require('../detect-deps');
338
+ const result = await autoInstallDeps();
339
+ expect(result.ok).toBe(true);
340
+ expect(result.installed).toEqual([]);
341
+ });
342
+
343
+ it('creates skills directory if it does not exist', async () => {
344
+ const { autoInstallDeps } = require('../detect-deps');
345
+ vi.spyOn(require('child_process'), 'spawnSync').mockImplementation((cmd, args) => {
346
+ const destPath = args[args.length - 1];
347
+ fs.mkdirSync(destPath, { recursive: true });
348
+ fs.writeFileSync(path.join(destPath, 'package.json'), JSON.stringify({ version: '2.0.0' }));
349
+ return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
350
+ });
351
+
352
+ const result = await autoInstallDeps();
353
+ expect(result.ok).toBe(true);
354
+ expect(result.installed).toContain('superpowers');
355
+ expect(result.installed).toContain('gstack');
356
+
357
+ vi.restoreAllMocks();
358
+ });
359
+
360
+ it('returns errors when git clone fails', async () => {
361
+ const { autoInstallDeps } = require('../detect-deps');
362
+ vi.spyOn(require('child_process'), 'spawnSync').mockImplementation(() => {
363
+ throw new Error('git not found');
364
+ });
365
+
366
+ const result = await autoInstallDeps();
367
+ expect(result.ok).toBe(false);
368
+ expect(result.errors.length).toBeGreaterThan(0);
369
+ expect(result.errors[0].message).toContain('git not found');
370
+
371
+ vi.restoreAllMocks();
372
+ });
373
+
374
+ it('returns errors when git clone exits non-zero (Issue #155)', async () => {
375
+ const { autoInstallDeps } = require('../detect-deps');
376
+ vi.spyOn(require('child_process'), 'spawnSync').mockImplementation(() => ({
377
+ status: 128,
378
+ stdout: Buffer.from(''),
379
+ stderr: Buffer.from('fatal: repository not found'),
380
+ }));
381
+
382
+ const result = await autoInstallDeps();
383
+ expect(result.ok).toBe(false);
384
+ expect(result.errors.length).toBeGreaterThan(0);
385
+ expect(result.errors[0].message).toMatch(/repository not found|git clone failed|exit/i);
386
+
387
+ vi.restoreAllMocks();
388
+ });
389
+
390
+ it('passes repoUrl as argv[] argument, never via shell string (Issue #155)', async () => {
391
+ const { autoInstallDeps } = require('../detect-deps');
392
+ const spy = vi.spyOn(require('child_process'), 'spawnSync').mockImplementation((cmd, args) => {
393
+ const destPath = args[args.length - 1];
394
+ fs.mkdirSync(destPath, { recursive: true });
395
+ fs.writeFileSync(path.join(destPath, 'package.json'), JSON.stringify({ version: '2.0.0' }));
396
+ return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
397
+ });
398
+
399
+ await autoInstallDeps();
400
+
401
+ expect(spy).toHaveBeenCalled();
402
+ for (const call of spy.mock.calls) {
403
+ const [cmd, args, opts] = call;
404
+ expect(cmd).toBe('git');
405
+ expect(Array.isArray(args)).toBe(true);
406
+ expect(args[0]).toBe('clone');
407
+ expect(opts && opts.shell).not.toBe(true);
408
+ }
409
+
410
+ vi.restoreAllMocks();
411
+ });
242
412
  });
243
413
  });
@@ -27,6 +27,7 @@ describe('doctor', () => {
27
27
  delete require.cache[require.resolve('../uninstall')];
28
28
  delete require.cache[require.resolve('../init')];
29
29
  delete require.cache[require.resolve('../detect-deps.js')];
30
+ delete require.cache[require.resolve('../shared-paths')];
30
31
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
31
32
  warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
32
33
  errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -11,6 +11,8 @@ import {
11
11
  readTailEntries,
12
12
  computeStats,
13
13
  rotateIfNeeded,
14
+ parseCliOptions,
15
+ getCommitHash,
14
16
  type GateAuditEntry,
15
17
  } from '../gate-audit';
16
18
 
@@ -114,6 +116,28 @@ describe('gate-audit', () => {
114
116
  const tail = readTailEntries(undefined, repoRoot);
115
117
  expect(tail.length).toBe(20);
116
118
  });
119
+
120
+ it('should skip malformed JSON lines', () => {
121
+ const repoRoot = join(TEST_DIR, 'malformed-tail');
122
+ const logDir = join(repoRoot, '.xp-gate');
123
+ mkdirSync(logDir, { recursive: true });
124
+ writeFileSync(
125
+ join(logDir, 'audit.jsonl'),
126
+ `not-json\n${JSON.stringify(makeEntry({ repo_path: repoRoot, gate_id: 'gate-valid' }))}\n`,
127
+ 'utf8',
128
+ );
129
+ const tail = readTailEntries(10, repoRoot);
130
+ expect(tail.length).toBe(1);
131
+ expect(tail[0].gate_id).toBe('gate-valid');
132
+ });
133
+
134
+ it('should return empty array for empty log file', () => {
135
+ const repoRoot = join(TEST_DIR, 'empty-log');
136
+ const logDir = join(repoRoot, '.xp-gate');
137
+ mkdirSync(logDir, { recursive: true });
138
+ writeFileSync(join(logDir, 'audit.jsonl'), '', 'utf8');
139
+ expect(readTailEntries(10, repoRoot)).toEqual([]);
140
+ });
117
141
  });
118
142
 
119
143
  describe('computeStats', () => {
@@ -153,6 +177,41 @@ describe('gate-audit', () => {
153
177
  const stats = computeStats(repoRoot);
154
178
  expect(stats[0].avg_issues).toBe(2.67);
155
179
  });
180
+
181
+ it('should return empty array for empty stats log', () => {
182
+ const repoRoot = join(TEST_DIR, 'empty-stats-log');
183
+ const logDir = join(repoRoot, '.xp-gate');
184
+ mkdirSync(logDir, { recursive: true });
185
+ writeFileSync(join(logDir, 'audit.jsonl'), '', 'utf8');
186
+ expect(computeStats(repoRoot)).toEqual([]);
187
+ });
188
+
189
+ it('should skip malformed lines during stats aggregation', () => {
190
+ const repoRoot = join(TEST_DIR, 'malformed-stats');
191
+ const logDir = join(repoRoot, '.xp-gate');
192
+ mkdirSync(logDir, { recursive: true });
193
+ writeFileSync(
194
+ join(logDir, 'audit.jsonl'),
195
+ `${JSON.stringify(makeEntry({ repo_path: repoRoot, gate_id: 'gate-1' }))}\n{bad json}\n`,
196
+ 'utf8',
197
+ );
198
+ const stats = computeStats(repoRoot);
199
+ expect(stats).toHaveLength(1);
200
+ expect(stats[0].gate_id).toBe('gate-1');
201
+ });
202
+ });
203
+
204
+ describe('CLI helpers', () => {
205
+ it('should parse CLI options and skip dangling flags', () => {
206
+ expect(parseCliOptions(['--gate-id', 'gate-1', '--passed', 'true', '--dangling'])).toEqual({
207
+ 'gate-id': 'gate-1',
208
+ passed: 'true',
209
+ });
210
+ });
211
+
212
+ it('should return a commit hash string or unknown', () => {
213
+ expect(typeof getCommitHash()).toBe('string');
214
+ });
156
215
  });
157
216
 
158
217
  describe('rotateIfNeeded', () => {
@@ -24,9 +24,20 @@ describe('init', () => {
24
24
  vi.resetModules();
25
25
  delete require.cache[require.resolve('../init')];
26
26
  delete require.cache[require.resolve('../detect-deps.js')];
27
+ delete require.cache[require.resolve('../shared-paths')];
27
28
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
28
29
  warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
29
30
  errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
31
+ vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
32
+ if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
33
+ return {
34
+ status: 1,
35
+ stdout: Buffer.from(''),
36
+ stderr: Buffer.from('git clone disabled in test (default stub)'),
37
+ };
38
+ }
39
+ return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
40
+ });
30
41
  });
31
42
 
32
43
  afterEach(() => {
@@ -55,7 +66,21 @@ describe('init', () => {
55
66
  });
56
67
  }
57
68
 
69
+ function stubSpawnGitClone(status = 0, stderr = '') {
70
+ vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
71
+ if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
72
+ return {
73
+ status,
74
+ stdout: Buffer.from(''),
75
+ stderr: Buffer.from(stderr),
76
+ };
77
+ }
78
+ return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
79
+ });
80
+ }
81
+
58
82
  function mockExecSuccess() {
83
+ stubSpawnGitClone(1, 'git clone disabled in test');
59
84
  execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
60
85
  if (cmd === 'git rev-parse --git-dir') {
61
86
  return path.join(tmpProject, '.git') + '\n';
@@ -68,6 +93,7 @@ describe('init', () => {
68
93
  }
69
94
 
70
95
  function mockExecGitDirOnly() {
96
+ stubSpawnGitClone(1, 'git clone disabled in test');
71
97
  execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
72
98
  if (cmd === 'git rev-parse --git-dir') {
73
99
  return path.join(tmpProject, '.git') + '\n';
@@ -80,6 +106,7 @@ describe('init', () => {
80
106
  }
81
107
 
82
108
  function mockExecFail() {
109
+ stubSpawnGitClone(1, 'git clone disabled in test');
83
110
  execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation(() => {
84
111
  throw new Error('Not a git repo');
85
112
  });
@@ -102,6 +129,16 @@ describe('init', () => {
102
129
  });
103
130
 
104
131
  it('init([]) with missing superpowers warns Missing dependencies', async () => {
132
+ vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
133
+ if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
134
+ return {
135
+ status: 1,
136
+ stdout: Buffer.from(''),
137
+ stderr: Buffer.from('git clone not available in test'),
138
+ };
139
+ }
140
+ return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
141
+ });
105
142
  const { init } = require('../init');
106
143
  const result = await init([]);
107
144
  expect(result).toBe(0);
@@ -110,7 +147,16 @@ describe('init', () => {
110
147
  });
111
148
 
112
149
  it('init([]) with versionMismatch warns version detail', async () => {
113
- // superpowers too old, gstack good
150
+ vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
151
+ if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
152
+ return {
153
+ status: 1,
154
+ stdout: Buffer.from(''),
155
+ stderr: Buffer.from('git clone not available in test'),
156
+ };
157
+ }
158
+ return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
159
+ });
114
160
  const sp = path.join(skillsDir(), 'superpowers');
115
161
  fs.mkdirSync(sp, { recursive: true });
116
162
  fs.writeFileSync(path.join(sp, 'package.json'), JSON.stringify({ version: '0.0.1' }));