xp-gate 0.5.1

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 (90) hide show
  1. package/adapter-common.sh +192 -0
  2. package/adapters/cpp.sh +76 -0
  3. package/adapters/dart.sh +41 -0
  4. package/adapters/flutter.sh +41 -0
  5. package/adapters/go.sh +59 -0
  6. package/adapters/iac.sh +189 -0
  7. package/adapters/java.sh +191 -0
  8. package/adapters/kotlin.sh +77 -0
  9. package/adapters/objectivec.sh +38 -0
  10. package/adapters/powershell.sh +138 -0
  11. package/adapters/python.sh +104 -0
  12. package/adapters/shell.sh +55 -0
  13. package/adapters/swift.sh +44 -0
  14. package/adapters/typescript.sh +61 -0
  15. package/bin/xp-gate.js +157 -0
  16. package/hooks/adapter-common.sh +192 -0
  17. package/hooks/pre-commit +1667 -0
  18. package/hooks/pre-push +395 -0
  19. package/lib/__tests__/detect-deps.test.js +209 -0
  20. package/lib/__tests__/doctor.test.js +448 -0
  21. package/lib/__tests__/download-skill.test.js +281 -0
  22. package/lib/__tests__/init.test.js +327 -0
  23. package/lib/__tests__/install-skill.test.js +326 -0
  24. package/lib/__tests__/migrate.test.js +212 -0
  25. package/lib/__tests__/rollback.test.js +183 -0
  26. package/lib/__tests__/ui-detector.test.ts +200 -0
  27. package/lib/__tests__/uninstall-skill.test.js +189 -0
  28. package/lib/__tests__/uninstall.test.js +589 -0
  29. package/lib/__tests__/update-skill.test.js +276 -0
  30. package/lib/detect-deps.js +157 -0
  31. package/lib/doctor.js +370 -0
  32. package/lib/download-skill.js +96 -0
  33. package/lib/init.js +367 -0
  34. package/lib/install-skill.js +184 -0
  35. package/lib/migrate.js +120 -0
  36. package/lib/rollback.js +78 -0
  37. package/lib/ui-detector.ts +99 -0
  38. package/lib/uninstall-skill.js +69 -0
  39. package/lib/uninstall.js +401 -0
  40. package/lib/update-skill.js +90 -0
  41. package/package.json +39 -0
  42. package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
  43. package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
  44. package/plugins/claude-code/bin/xp-gate-check +47 -0
  45. package/plugins/claude-code/hooks/hooks.json +37 -0
  46. package/skills/delphi-review/.delphi-config.json.example +45 -0
  47. package/skills/delphi-review/AGENTS.md +54 -0
  48. package/skills/delphi-review/INSTALL.md +152 -0
  49. package/skills/delphi-review/SKILL.md +371 -0
  50. package/skills/delphi-review/evals/evals.json +82 -0
  51. package/skills/delphi-review/opencode.json.delphi.example +56 -0
  52. package/skills/delphi-review/references/code-walkthrough.md +486 -0
  53. package/skills/ralph-loop/SKILL.md +330 -0
  54. package/skills/ralph-loop/evals/evals.json +311 -0
  55. package/skills/ralph-loop/evolution-history.json +59 -0
  56. package/skills/ralph-loop/evolution-log.md +16 -0
  57. package/skills/ralph-loop/references/components/memory.md +55 -0
  58. package/skills/ralph-loop/references/components/middleware.md +54 -0
  59. package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  60. package/skills/ralph-loop/references/components/system-prompt.md +24 -0
  61. package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  62. package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  63. package/skills/ralph-loop/templates/progress-log.md +36 -0
  64. package/skills/sprint-flow/SKILL.md +600 -0
  65. package/skills/sprint-flow/evals/evals.json +78 -0
  66. package/skills/sprint-flow/evolution-history.json +39 -0
  67. package/skills/sprint-flow/evolution-log.md +23 -0
  68. package/skills/sprint-flow/references/components/memory.md +87 -0
  69. package/skills/sprint-flow/references/components/middleware.md +72 -0
  70. package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  71. package/skills/sprint-flow/references/components/system-prompt.md +27 -0
  72. package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  73. package/skills/sprint-flow/references/phase-0-think.md +115 -0
  74. package/skills/sprint-flow/references/phase-1-plan.md +178 -0
  75. package/skills/sprint-flow/references/phase-2-build.md +198 -0
  76. package/skills/sprint-flow/references/phase-3-review.md +213 -0
  77. package/skills/sprint-flow/references/phase-4-uat.md +125 -0
  78. package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  79. package/skills/sprint-flow/references/phase-6-ship.md +193 -0
  80. package/skills/sprint-flow/references/phase-7-land.md +140 -0
  81. package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  82. package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  83. package/skills/sprint-flow/templates/pain-document-template.md +115 -0
  84. package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  85. package/skills/test-specification-alignment/AGENTS.md +59 -0
  86. package/skills/test-specification-alignment/SKILL.md +605 -0
  87. package/skills/test-specification-alignment/evals/evals.json +75 -0
  88. package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  89. package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  90. package/skills/test-specification-alignment/references/specification-format.md +348 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @test update-skill
3
+ * @intent Verify updateSkill() handles check/all/single-name modes, config parsing, and delegates to installSkill correctly
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ describe('update-skill', () => {
10
+ let tmpHome;
11
+ let originalHome;
12
+ let logSpy;
13
+ let errorSpy;
14
+ let installSkillMock;
15
+
16
+ beforeEach(() => {
17
+ originalHome = process.env.HOME;
18
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-up-'));
19
+ process.env.HOME = tmpHome;
20
+
21
+ vi.resetModules();
22
+ delete require.cache[require.resolve('../update-skill')];
23
+ delete require.cache[require.resolve('../install-skill.js')];
24
+
25
+ // Inject mock install-skill module into the require.cache BEFORE update-skill
26
+ // pulls it in (it lazy-requires it inside updateSingleSkill).
27
+ installSkillMock = vi.fn().mockResolvedValue(0);
28
+ const installSkillPath = require.resolve('../install-skill.js');
29
+ require.cache[installSkillPath] = {
30
+ id: installSkillPath,
31
+ filename: installSkillPath,
32
+ loaded: true,
33
+ exports: { installSkill: installSkillMock },
34
+ };
35
+
36
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
37
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
38
+ });
39
+
40
+ afterEach(() => {
41
+ process.env.HOME = originalHome;
42
+ fs.rmSync(tmpHome, { recursive: true, force: true });
43
+ delete require.cache[require.resolve('../install-skill.js')];
44
+ delete require.cache[require.resolve('../update-skill')];
45
+ vi.restoreAllMocks();
46
+ });
47
+
48
+ function configDir() {
49
+ return path.join(tmpHome, '.config', 'xp-gate');
50
+ }
51
+
52
+ function skillsDir() {
53
+ return path.join(tmpHome, '.config', 'opencode', 'skills');
54
+ }
55
+
56
+ function writeConfig(skills) {
57
+ fs.mkdirSync(configDir(), { recursive: true });
58
+ fs.writeFileSync(
59
+ path.join(configDir(), 'xp-gate.json'),
60
+ JSON.stringify({ installedSkills: skills })
61
+ );
62
+ }
63
+
64
+ function makeSkillDir(name) {
65
+ const dir = path.join(skillsDir(), name);
66
+ fs.mkdirSync(dir, { recursive: true });
67
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), 'old content');
68
+ return dir;
69
+ }
70
+
71
+ // ----- name + flags validation -----
72
+
73
+ it('returns 1 + console.error when name is null and no flags', async () => {
74
+ const { updateSkill } = require('../update-skill');
75
+ const result = await updateSkill(null);
76
+ expect(result).toBe(1);
77
+ expect(errorSpy).toHaveBeenCalledWith('Error: Skill name required');
78
+ expect(errorSpy).toHaveBeenCalledWith(
79
+ 'Usage: xp-gate update-skill <name> or --all'
80
+ );
81
+ });
82
+
83
+ it('returns 1 + console.error when name is undefined and no flags', async () => {
84
+ const { updateSkill } = require('../update-skill');
85
+ const result = await updateSkill();
86
+ expect(result).toBe(1);
87
+ expect(errorSpy).toHaveBeenCalledWith('Error: Skill name required');
88
+ });
89
+
90
+ it('returns 1 + "is not installed" when name not in config.installedSkills', async () => {
91
+ writeConfig({}); // empty
92
+ const { updateSkill } = require('../update-skill');
93
+ const result = await updateSkill('foo');
94
+ expect(result).toBe(1);
95
+ expect(errorSpy).toHaveBeenCalledWith('Error: foo is not installed');
96
+ });
97
+
98
+ it('returns 1 + "is not installed" when config file is missing entirely', async () => {
99
+ const { updateSkill } = require('../update-skill');
100
+ const result = await updateSkill('foo');
101
+ expect(result).toBe(1);
102
+ expect(errorSpy).toHaveBeenCalledWith('Error: foo is not installed');
103
+ });
104
+
105
+ // ----- check mode -----
106
+
107
+ it('check=true prints each installed skill with its version, returns 0', async () => {
108
+ writeConfig({
109
+ foo: { version: '1.2.3' },
110
+ bar: { version: '2.0.0' },
111
+ });
112
+ const { updateSkill } = require('../update-skill');
113
+ const result = await updateSkill(null, { check: true });
114
+ expect(result).toBe(0);
115
+ expect(logSpy).toHaveBeenCalledWith('Checking for updates...');
116
+ expect(logSpy).toHaveBeenCalledWith(' foo: 1.2.3');
117
+ expect(logSpy).toHaveBeenCalledWith(' bar: 2.0.0');
118
+ expect(logSpy).toHaveBeenCalledWith('Update check complete');
119
+ });
120
+
121
+ it('check=true with skill missing version falls back to "unknown"', async () => {
122
+ writeConfig({ baz: {} }); // no version field
123
+ const { updateSkill } = require('../update-skill');
124
+ const result = await updateSkill(null, { check: true });
125
+ expect(result).toBe(0);
126
+ expect(logSpy).toHaveBeenCalledWith(' baz: unknown');
127
+ });
128
+
129
+ it('check=true with empty config returns 0 and prints only headers', async () => {
130
+ const { updateSkill } = require('../update-skill');
131
+ const result = await updateSkill(null, { check: true });
132
+ expect(result).toBe(0);
133
+ expect(logSpy).toHaveBeenCalledWith('Checking for updates...');
134
+ expect(logSpy).toHaveBeenCalledWith('Update check complete');
135
+ });
136
+
137
+ // ----- all mode -----
138
+
139
+ it('all=true with empty skills returns 0 and prints "Updating all skills..."', async () => {
140
+ const { updateSkill } = require('../update-skill');
141
+ const result = await updateSkill(null, { all: true });
142
+ expect(result).toBe(0);
143
+ expect(logSpy).toHaveBeenCalledWith('Updating all skills...');
144
+ expect(installSkillMock).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it('all=true updates every installed skill (calls installSkill each), returns 0 on success', async () => {
148
+ writeConfig({
149
+ foo: { version: '1.0.0' },
150
+ bar: { version: '1.0.0' },
151
+ });
152
+ const { updateSkill } = require('../update-skill');
153
+ const result = await updateSkill(null, { all: true });
154
+ expect(result).toBe(0);
155
+ expect(installSkillMock).toHaveBeenCalledTimes(2);
156
+ expect(installSkillMock).toHaveBeenCalledWith('foo', {
157
+ force: true,
158
+ verbose: false,
159
+ });
160
+ expect(installSkillMock).toHaveBeenCalledWith('bar', {
161
+ force: true,
162
+ verbose: false,
163
+ });
164
+ expect(logSpy).toHaveBeenCalledWith('Updating foo...');
165
+ expect(logSpy).toHaveBeenCalledWith('Updating bar...');
166
+ expect(logSpy).toHaveBeenCalledWith('✓ foo updated');
167
+ expect(logSpy).toHaveBeenCalledWith('✓ bar updated');
168
+ });
169
+
170
+ it('all=true returns 1 when at least one installSkill rejects', async () => {
171
+ writeConfig({
172
+ good: { version: '1.0.0' },
173
+ bad: { version: '1.0.0' },
174
+ });
175
+ installSkillMock.mockImplementation(async (name) => {
176
+ if (name === 'bad') throw new Error('boom');
177
+ return 0;
178
+ });
179
+ const { updateSkill } = require('../update-skill');
180
+ const result = await updateSkill(null, { all: true });
181
+ expect(result).toBe(1);
182
+ expect(errorSpy).toHaveBeenCalledWith('Failed to update bad: boom');
183
+ expect(logSpy).toHaveBeenCalledWith('✓ good updated');
184
+ });
185
+
186
+ it('all=true propagates verbose flag into installSkill options', async () => {
187
+ writeConfig({ foo: { version: '1.0.0' } });
188
+ const { updateSkill } = require('../update-skill');
189
+ const result = await updateSkill(null, { all: true, verbose: true });
190
+ expect(result).toBe(0);
191
+ expect(installSkillMock).toHaveBeenCalledWith('foo', {
192
+ force: true,
193
+ verbose: true,
194
+ });
195
+ });
196
+
197
+ // ----- single skill mode -----
198
+
199
+ it('updateSkill(name) calls installSkill and returns its result (0=success)', async () => {
200
+ writeConfig({ foo: { version: '1.0.0' } });
201
+ const { updateSkill } = require('../update-skill');
202
+ const result = await updateSkill('foo');
203
+ expect(result).toBe(0);
204
+ expect(installSkillMock).toHaveBeenCalledWith('foo', {
205
+ force: true,
206
+ verbose: false,
207
+ });
208
+ expect(logSpy).toHaveBeenCalledWith('Updating foo...');
209
+ expect(logSpy).toHaveBeenCalledWith('✓ foo updated');
210
+ });
211
+
212
+ it('updateSkill(name) returns installSkill result when non-zero (no success log)', async () => {
213
+ writeConfig({ foo: { version: '1.0.0' } });
214
+ installSkillMock.mockResolvedValue(2);
215
+ const { updateSkill } = require('../update-skill');
216
+ const result = await updateSkill('foo');
217
+ expect(result).toBe(2);
218
+ expect(logSpy).not.toHaveBeenCalledWith('✓ foo updated');
219
+ });
220
+
221
+ it('removes existing targetDir before invoking installSkill', async () => {
222
+ writeConfig({ foo: { version: '1.0.0' } });
223
+ const dir = makeSkillDir('foo');
224
+ expect(fs.existsSync(dir)).toBe(true);
225
+
226
+ let dirExistedDuringInstall = true;
227
+ installSkillMock.mockImplementation(async () => {
228
+ dirExistedDuringInstall = fs.existsSync(dir);
229
+ return 0;
230
+ });
231
+
232
+ const { updateSkill } = require('../update-skill');
233
+ const result = await updateSkill('foo');
234
+ expect(result).toBe(0);
235
+ expect(dirExistedDuringInstall).toBe(false); // rm'd before installSkill called
236
+ });
237
+
238
+ it('does not crash when targetDir does not exist', async () => {
239
+ writeConfig({ foo: { version: '1.0.0' } });
240
+ // No skill dir on disk
241
+ const { updateSkill } = require('../update-skill');
242
+ const result = await updateSkill('foo');
243
+ expect(result).toBe(0);
244
+ expect(installSkillMock).toHaveBeenCalled();
245
+ });
246
+
247
+ // ----- getConfig fallback paths -----
248
+
249
+ it('getConfig returns {} when config file does not exist (single-name path → not installed)', async () => {
250
+ // exercised via "is not installed" — config absent ⇒ installedSkills undefined ⇒ {}
251
+ const { updateSkill } = require('../update-skill');
252
+ const result = await updateSkill('anything');
253
+ expect(result).toBe(1);
254
+ expect(errorSpy).toHaveBeenCalledWith('Error: anything is not installed');
255
+ });
256
+
257
+ it('getConfig returns {} on JSON parse error (catch swallows)', async () => {
258
+ fs.mkdirSync(configDir(), { recursive: true });
259
+ fs.writeFileSync(path.join(configDir(), 'xp-gate.json'), '{not valid json');
260
+ const { updateSkill } = require('../update-skill');
261
+ const result = await updateSkill('foo');
262
+ expect(result).toBe(1);
263
+ expect(errorSpy).toHaveBeenCalledWith('Error: foo is not installed');
264
+ });
265
+
266
+ it('getConfig parses valid config (single-name path succeeds)', async () => {
267
+ writeConfig({ foo: { version: '1.0.0' } });
268
+ const { updateSkill } = require('../update-skill');
269
+ const result = await updateSkill('foo');
270
+ expect(result).toBe(0);
271
+ expect(installSkillMock).toHaveBeenCalledWith('foo', {
272
+ force: true,
273
+ verbose: false,
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,157 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
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');
15
+
16
+ const REQUIRED_DEPS = [
17
+ { name: 'superpowers', minVersion: '1.0.0' },
18
+ { name: 'gstack', minVersion: '1.0.0' }
19
+ ];
20
+
21
+ /**
22
+ * Check if bash is available on the system.
23
+ * XP-Gate hooks are bash scripts — Windows users need Git Bash installed.
24
+ * @returns {{ok: boolean, path?: string, message?: string}}
25
+ */
26
+ function checkBash() {
27
+ try {
28
+ // Try 'bash' first (works on Linux/macOS and when Git Bash is in PATH)
29
+ const result = execSync('bash --version', {
30
+ encoding: 'utf8',
31
+ stdio: ['ignore', 'pipe', 'pipe']
32
+ });
33
+ const versionMatch = result.match(/version\s+([^\s]+)/);
34
+ return {
35
+ ok: true,
36
+ path: 'bash',
37
+ version: versionMatch ? versionMatch[1] : 'unknown'
38
+ };
39
+ } catch (e) {
40
+ // On Windows with Git Bash, try common locations
41
+ const bashPaths = [
42
+ // Git for Windows
43
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
44
+ 'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
45
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
46
+ 'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
47
+ // MSYS2
48
+ 'C:\\msys64\\usr\\bin\\bash.exe',
49
+ // Cygwin
50
+ 'C:\\cygwin64\\bin\\bash.exe',
51
+ ];
52
+
53
+ for (const bashPath of bashPaths) {
54
+ try {
55
+ const result = execSync(`"${bashPath}" --version`, {
56
+ encoding: 'utf8',
57
+ stdio: ['ignore', 'pipe', 'pipe']
58
+ });
59
+ const versionMatch = result.match(/version\s+([^\s]+)/);
60
+ return {
61
+ ok: true,
62
+ path: bashPath,
63
+ version: versionMatch ? versionMatch[1] : 'unknown'
64
+ };
65
+ } catch {
66
+ // Try next path
67
+ }
68
+ }
69
+
70
+ // Windows-specific guidance
71
+ if (process.platform === 'win32') {
72
+ return {
73
+ ok: false,
74
+ message: 'bash not found. Windows users must install [Git for Windows](https://git-scm.com/download/win).\n' +
75
+ ' During installation, ensure "Git Bash Here" is checked — this adds bash.exe to PATH.\n' +
76
+ ' After installation, restart your terminal and run `npm install` again.'
77
+ };
78
+ }
79
+
80
+ return {
81
+ ok: false,
82
+ message: 'bash not found. Please install bash and ensure it is in PATH.'
83
+ };
84
+ }
85
+ }
86
+
87
+ async function checkDeps() {
88
+ for (const dep of REQUIRED_DEPS) {
89
+ const possiblePaths = [
90
+ path.join(SKILLS_DIR, dep.name),
91
+ path.join(OPENCODE_DIR, dep.name)
92
+ ];
93
+
94
+ let depDir = null;
95
+ for (const p of possiblePaths) {
96
+ if (fs.existsSync(p)) {
97
+ depDir = p;
98
+ break;
99
+ }
100
+ }
101
+
102
+ if (!depDir) {
103
+ return { ok: false, missing: dep.name };
104
+ }
105
+
106
+ const version = await getSkillVersion(depDir);
107
+ if (version && compareVersions(version, dep.minVersion) < 0) {
108
+ return {
109
+ ok: false,
110
+ versionMismatch: {
111
+ name: dep.name,
112
+ required: dep.minVersion,
113
+ found: version
114
+ }
115
+ };
116
+ }
117
+ }
118
+
119
+ return { ok: true };
120
+ }
121
+
122
+ async function getSkillVersion(skillDir) {
123
+ const pkgFile = path.join(skillDir, 'package.json');
124
+ if (fs.existsSync(pkgFile)) {
125
+ try {
126
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
127
+ return pkg.version;
128
+ } catch {}
129
+ }
130
+
131
+ const skillFile = path.join(skillDir, 'SKILL.md');
132
+ if (fs.existsSync(skillFile)) {
133
+ const content = fs.readFileSync(skillFile, 'utf8');
134
+ const versionMatch = content.match(/^version:\s*"?([0-9]+\.[0-9]+\.[0-9]+)"?/m);
135
+ if (versionMatch) {
136
+ return versionMatch[1];
137
+ }
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ function compareVersions(a, b) {
144
+ const partsA = a.split('.').map(Number);
145
+ const partsB = b.split('.').map(Number);
146
+
147
+ for (let i = 0; i < 3; i++) {
148
+ const partA = partsA[i] || 0;
149
+ const partB = partsB[i] || 0;
150
+ if (partA > partB) return 1;
151
+ if (partA < partB) return -1;
152
+ }
153
+
154
+ return 0;
155
+ }
156
+
157
+ module.exports = { checkDeps, checkBash };