@boyingliu01/xp-gate 0.7.1 → 0.8.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.
- package/lib/__tests__/detect-deps.test.js +45 -12
- package/lib/__tests__/init.test.js +42 -11
- package/lib/__tests__/ui-detector.test.ts +67 -21
- package/lib/detect-deps.js +70 -68
- package/lib/ui-detector.ts +17 -11
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +904 -0
- package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +45 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +66 -0
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +152 -0
- package/plugins/claude-code/skills/delphi-review/SKILL.md +454 -0
- package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +501 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/memory.md +55 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/middleware.md +54 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/plugins/claude-code/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/claude-code/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +68 -0
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1248 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +72 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +203 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
- package/plugins/claude-code/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/claude-code/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
- package/plugins/claude-code/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/claude-code/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/claude-code/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/plugins/claude-code/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +71 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +702 -0
- package/plugins/claude-code/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/claude-code/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/claude-code/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +277 -0
- package/plugins/opencode/README.md +38 -0
- package/plugins/opencode/index.ts +85 -0
- package/plugins/opencode/package.json +18 -0
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +904 -0
- package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +45 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +66 -0
- package/plugins/opencode/skills/delphi-review/INSTALL.md +152 -0
- package/plugins/opencode/skills/delphi-review/SKILL.md +454 -0
- package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +501 -0
- package/plugins/opencode/skills/ralph-loop/references/components/memory.md +55 -0
- package/plugins/opencode/skills/ralph-loop/references/components/middleware.md +54 -0
- package/plugins/opencode/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/plugins/opencode/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/plugins/opencode/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/plugins/opencode/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/opencode/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +68 -0
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1248 -0
- package/plugins/opencode/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +72 -0
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/plugins/opencode/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +203 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
- package/plugins/opencode/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/opencode/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
- package/plugins/opencode/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/opencode/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/opencode/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/plugins/opencode/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/plugins/opencode/skills/test-driven-development/SKILL.md +71 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +702 -0
- package/plugins/opencode/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/opencode/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/opencode/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +277 -0
- package/plugins/opencode/tsconfig.json +15 -0
- package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +904 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +45 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +66 -0
- package/plugins/qoder/skills/delphi-review/INSTALL.md +152 -0
- package/plugins/qoder/skills/delphi-review/SKILL.md +454 -0
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/plugins/qoder/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/qoder/skills/ralph-loop/SKILL.md +501 -0
- package/plugins/qoder/skills/ralph-loop/references/components/memory.md +55 -0
- package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +54 -0
- package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/plugins/qoder/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/qoder/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +68 -0
- package/plugins/qoder/skills/sprint-flow/SKILL.md +1248 -0
- package/plugins/qoder/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +72 -0
- package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/plugins/qoder/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +203 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
- package/plugins/qoder/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/qoder/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
- package/plugins/qoder/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/qoder/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/qoder/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/plugins/qoder/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/plugins/qoder/skills/test-driven-development/SKILL.md +71 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/qoder/skills/test-specification-alignment/SKILL.md +702 -0
- package/plugins/qoder/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/qoder/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/qoder/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/qoder/skills/to-issues/SKILL.md +277 -0
- package/skills/admin-template-guidelines/SKILL.md +904 -0
- package/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/skills/sprint-flow/SKILL.md +26 -8
- package/skills/test-driven-development/SKILL.md +71 -0
- package/skills/to-issues/SKILL.md +277 -0
- package/skills/delphi-review/evals/evals.json +0 -82
- package/skills/ralph-loop/evals/evals.json +0 -311
- package/skills/ralph-loop/evolution-history.json +0 -59
- package/skills/ralph-loop/evolution-log.md +0 -16
- package/skills/sprint-flow/evals/evals.json +0 -130
- package/skills/sprint-flow/evolution-history.json +0 -39
- package/skills/sprint-flow/evolution-log.md +0 -23
- package/skills/test-specification-alignment/evals/evals.json +0 -75
|
@@ -342,20 +342,14 @@ describe('detect-deps', () => {
|
|
|
342
342
|
|
|
343
343
|
it('creates skills directory if it does not exist', async () => {
|
|
344
344
|
const { autoInstallDeps } = require('../detect-deps');
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (match) {
|
|
351
|
-
fs.mkdirSync(match[1], { recursive: true });
|
|
352
|
-
fs.writeFileSync(path.join(match[1], 'package.json'), JSON.stringify({ version: '2.0.0' }));
|
|
353
|
-
}
|
|
354
|
-
return '';
|
|
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('') };
|
|
355
350
|
});
|
|
356
351
|
|
|
357
352
|
const result = await autoInstallDeps();
|
|
358
|
-
// Should have attempted to install
|
|
359
353
|
expect(result.ok).toBe(true);
|
|
360
354
|
expect(result.installed).toContain('superpowers');
|
|
361
355
|
expect(result.installed).toContain('gstack');
|
|
@@ -365,7 +359,7 @@ describe('detect-deps', () => {
|
|
|
365
359
|
|
|
366
360
|
it('returns errors when git clone fails', async () => {
|
|
367
361
|
const { autoInstallDeps } = require('../detect-deps');
|
|
368
|
-
vi.spyOn(require('child_process'), '
|
|
362
|
+
vi.spyOn(require('child_process'), 'spawnSync').mockImplementation(() => {
|
|
369
363
|
throw new Error('git not found');
|
|
370
364
|
});
|
|
371
365
|
|
|
@@ -376,5 +370,44 @@ describe('detect-deps', () => {
|
|
|
376
370
|
|
|
377
371
|
vi.restoreAllMocks();
|
|
378
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
|
+
});
|
|
379
412
|
});
|
|
380
413
|
});
|
|
@@ -28,6 +28,16 @@ describe('init', () => {
|
|
|
28
28
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
29
29
|
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
30
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
|
+
});
|
|
31
41
|
});
|
|
32
42
|
|
|
33
43
|
afterEach(() => {
|
|
@@ -56,7 +66,21 @@ describe('init', () => {
|
|
|
56
66
|
});
|
|
57
67
|
}
|
|
58
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
|
+
|
|
59
82
|
function mockExecSuccess() {
|
|
83
|
+
stubSpawnGitClone(1, 'git clone disabled in test');
|
|
60
84
|
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
61
85
|
if (cmd === 'git rev-parse --git-dir') {
|
|
62
86
|
return path.join(tmpProject, '.git') + '\n';
|
|
@@ -69,6 +93,7 @@ describe('init', () => {
|
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
function mockExecGitDirOnly() {
|
|
96
|
+
stubSpawnGitClone(1, 'git clone disabled in test');
|
|
72
97
|
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
73
98
|
if (cmd === 'git rev-parse --git-dir') {
|
|
74
99
|
return path.join(tmpProject, '.git') + '\n';
|
|
@@ -81,6 +106,7 @@ describe('init', () => {
|
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
function mockExecFail() {
|
|
109
|
+
stubSpawnGitClone(1, 'git clone disabled in test');
|
|
84
110
|
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation(() => {
|
|
85
111
|
throw new Error('Not a git repo');
|
|
86
112
|
});
|
|
@@ -103,12 +129,15 @@ describe('init', () => {
|
|
|
103
129
|
});
|
|
104
130
|
|
|
105
131
|
it('init([]) with missing superpowers warns Missing dependencies', async () => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
};
|
|
110
139
|
}
|
|
111
|
-
return '';
|
|
140
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
112
141
|
});
|
|
113
142
|
const { init } = require('../init');
|
|
114
143
|
const result = await init([]);
|
|
@@ -118,14 +147,16 @@ describe('init', () => {
|
|
|
118
147
|
});
|
|
119
148
|
|
|
120
149
|
it('init([]) with versionMismatch warns version detail', async () => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
};
|
|
125
157
|
}
|
|
126
|
-
return '';
|
|
158
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
127
159
|
});
|
|
128
|
-
// superpowers too old, gstack good
|
|
129
160
|
const sp = path.join(skillsDir(), 'superpowers');
|
|
130
161
|
fs.mkdirSync(sp, { recursive: true });
|
|
131
162
|
fs.writeFileSync(path.join(sp, 'package.json'), JSON.stringify({ version: '0.0.1' }));
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @test ui-detector
|
|
3
3
|
* @intent Verify detectUiSprint correctly identifies UI file changes
|
|
4
|
+
* @covers Issue #155 — getChangedFiles must use spawnSync (array args) to prevent
|
|
5
|
+
* shell-meta injection via baseBranch parameter
|
|
4
6
|
*/
|
|
5
7
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
-
import {
|
|
8
|
+
import { spawnSync } from 'child_process';
|
|
7
9
|
|
|
8
10
|
vi.mock('child_process', () => ({
|
|
9
|
-
|
|
11
|
+
spawnSync: vi.fn(),
|
|
12
|
+
// Keep execSync as a vi.fn so any unintentional callers fail loudly
|
|
13
|
+
execSync: vi.fn(() => {
|
|
14
|
+
throw new Error('execSync must not be used: command injection risk (Issue #155)');
|
|
15
|
+
}),
|
|
10
16
|
}));
|
|
11
17
|
|
|
12
|
-
const
|
|
18
|
+
const mockSpawnSync = vi.mocked(spawnSync);
|
|
19
|
+
|
|
20
|
+
function mockSpawnReturn(stdout: string) {
|
|
21
|
+
mockSpawnSync.mockReturnValue({
|
|
22
|
+
pid: 1,
|
|
23
|
+
stdout: Buffer.from(stdout),
|
|
24
|
+
stderr: Buffer.from(''),
|
|
25
|
+
status: 0,
|
|
26
|
+
signal: null,
|
|
27
|
+
output: [null, stdout, ''],
|
|
28
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
29
|
+
}
|
|
13
30
|
|
|
14
31
|
describe('ui-detector', () => {
|
|
15
32
|
beforeEach(() => {
|
|
@@ -18,7 +35,7 @@ describe('ui-detector', () => {
|
|
|
18
35
|
|
|
19
36
|
describe('detectUiSprint', () => {
|
|
20
37
|
it('should return false for empty diff', async () => {
|
|
21
|
-
|
|
38
|
+
mockSpawnReturn('');
|
|
22
39
|
const { detectUiSprint } = await import('../ui-detector');
|
|
23
40
|
const result = detectUiSprint();
|
|
24
41
|
expect(result.isUiSprint).toBe(false);
|
|
@@ -27,14 +44,14 @@ describe('ui-detector', () => {
|
|
|
27
44
|
});
|
|
28
45
|
|
|
29
46
|
it('should return false for pure backend changes', async () => {
|
|
30
|
-
|
|
47
|
+
mockSpawnReturn('src/auth.ts\nsrc/db.ts\n');
|
|
31
48
|
const { detectUiSprint } = await import('../ui-detector');
|
|
32
49
|
const result = detectUiSprint('main');
|
|
33
50
|
expect(result.isUiSprint).toBe(false);
|
|
34
51
|
});
|
|
35
52
|
|
|
36
53
|
it('should return true for template files in view directories', async () => {
|
|
37
|
-
|
|
54
|
+
mockSpawnReturn('views/index.njk\n');
|
|
38
55
|
const { detectUiSprint } = await import('../ui-detector');
|
|
39
56
|
const result = detectUiSprint();
|
|
40
57
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -43,7 +60,7 @@ describe('ui-detector', () => {
|
|
|
43
60
|
});
|
|
44
61
|
|
|
45
62
|
it('should return true for component files in view directories', async () => {
|
|
46
|
-
|
|
63
|
+
mockSpawnReturn('src/components/Button.tsx\n');
|
|
47
64
|
const { detectUiSprint } = await import('../ui-detector');
|
|
48
65
|
const result = detectUiSprint();
|
|
49
66
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -51,28 +68,28 @@ describe('ui-detector', () => {
|
|
|
51
68
|
});
|
|
52
69
|
|
|
53
70
|
it('should return false for component files NOT in view directories', async () => {
|
|
54
|
-
|
|
71
|
+
mockSpawnReturn('src/hooks/useAuth.tsx\n');
|
|
55
72
|
const { detectUiSprint } = await import('../ui-detector');
|
|
56
73
|
const result = detectUiSprint();
|
|
57
74
|
expect(result.isUiSprint).toBe(false);
|
|
58
75
|
});
|
|
59
76
|
|
|
60
77
|
it('should return true for style files in view directories', async () => {
|
|
61
|
-
|
|
78
|
+
mockSpawnReturn('views/styles/main.css\n');
|
|
62
79
|
const { detectUiSprint } = await import('../ui-detector');
|
|
63
80
|
const result = detectUiSprint();
|
|
64
81
|
expect(result.isUiSprint).toBe(true);
|
|
65
82
|
});
|
|
66
83
|
|
|
67
84
|
it('should return false for style files NOT in view directories', async () => {
|
|
68
|
-
|
|
85
|
+
mockSpawnReturn('src/index.css\n');
|
|
69
86
|
const { detectUiSprint } = await import('../ui-detector');
|
|
70
87
|
const result = detectUiSprint();
|
|
71
88
|
expect(result.isUiSprint).toBe(false);
|
|
72
89
|
});
|
|
73
90
|
|
|
74
91
|
it('should return true for mixed changes (backend + UI)', async () => {
|
|
75
|
-
|
|
92
|
+
mockSpawnReturn('src/auth.ts\nviews/login.html\n');
|
|
76
93
|
const { detectUiSprint } = await import('../ui-detector');
|
|
77
94
|
const result = detectUiSprint();
|
|
78
95
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -80,7 +97,7 @@ describe('ui-detector', () => {
|
|
|
80
97
|
});
|
|
81
98
|
|
|
82
99
|
it('should return true for renamed UI files', async () => {
|
|
83
|
-
|
|
100
|
+
mockSpawnReturn('views/a.html → views/b.html\n');
|
|
84
101
|
const { detectUiSprint } = await import('../ui-detector');
|
|
85
102
|
const result = detectUiSprint();
|
|
86
103
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -88,24 +105,25 @@ describe('ui-detector', () => {
|
|
|
88
105
|
});
|
|
89
106
|
|
|
90
107
|
it('should return false for pure documentation changes', async () => {
|
|
91
|
-
|
|
108
|
+
mockSpawnReturn('docs/README.md\n');
|
|
92
109
|
const { detectUiSprint } = await import('../ui-detector');
|
|
93
110
|
const result = detectUiSprint();
|
|
94
111
|
expect(result.isUiSprint).toBe(false);
|
|
95
112
|
});
|
|
96
113
|
|
|
97
114
|
it('should use main as default base branch', async () => {
|
|
98
|
-
|
|
115
|
+
mockSpawnReturn('');
|
|
99
116
|
const { detectUiSprint } = await import('../ui-detector');
|
|
100
117
|
detectUiSprint();
|
|
101
|
-
expect(
|
|
102
|
-
|
|
118
|
+
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
119
|
+
'git',
|
|
120
|
+
['diff', '--name-only', 'main..HEAD'],
|
|
103
121
|
expect.any(Object)
|
|
104
122
|
);
|
|
105
123
|
});
|
|
106
124
|
|
|
107
125
|
it('should handle git command failure gracefully', async () => {
|
|
108
|
-
|
|
126
|
+
mockSpawnSync.mockImplementation(() => {
|
|
109
127
|
throw new Error('git not available');
|
|
110
128
|
});
|
|
111
129
|
const { detectUiSprint } = await import('../ui-detector');
|
|
@@ -113,8 +131,36 @@ describe('ui-detector', () => {
|
|
|
113
131
|
expect(result.isUiSprint).toBe(false);
|
|
114
132
|
});
|
|
115
133
|
|
|
134
|
+
it('should treat non-zero git exit status as failure (Issue #155)', async () => {
|
|
135
|
+
mockSpawnSync.mockReturnValue({
|
|
136
|
+
pid: 1,
|
|
137
|
+
stdout: Buffer.from(''),
|
|
138
|
+
stderr: Buffer.from('fatal: bad revision'),
|
|
139
|
+
status: 128,
|
|
140
|
+
signal: null,
|
|
141
|
+
output: [null, '', 'fatal: bad revision'],
|
|
142
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
143
|
+
const { detectUiSprint } = await import('../ui-detector');
|
|
144
|
+
const result = detectUiSprint('main');
|
|
145
|
+
expect(result.isUiSprint).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should not interpret shell metacharacters in baseBranch (Issue #155)', async () => {
|
|
149
|
+
mockSpawnReturn('');
|
|
150
|
+
const { getChangedFiles } = await import('../ui-detector');
|
|
151
|
+
const malicious = 'main; touch /tmp/xp-gate-pwned';
|
|
152
|
+
getChangedFiles(malicious);
|
|
153
|
+
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
154
|
+
'git',
|
|
155
|
+
['diff', '--name-only', `${malicious}..HEAD`],
|
|
156
|
+
expect.objectContaining({ shell: false })
|
|
157
|
+
);
|
|
158
|
+
const callArgs = mockSpawnSync.mock.calls[0];
|
|
159
|
+
expect(Array.isArray(callArgs[1])).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
116
162
|
it('should handle multiple UI files across different types', async () => {
|
|
117
|
-
|
|
163
|
+
mockSpawnReturn('views/index.njk\nsrc/components/Button.tsx\nsrc/auth.ts\n');
|
|
118
164
|
const { detectUiSprint } = await import('../ui-detector');
|
|
119
165
|
const result = detectUiSprint();
|
|
120
166
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -124,21 +170,21 @@ describe('ui-detector', () => {
|
|
|
124
170
|
|
|
125
171
|
describe('getChangedFiles', () => {
|
|
126
172
|
it('parses git diff output into file array', async () => {
|
|
127
|
-
|
|
173
|
+
mockSpawnReturn('src/a.ts\nsrc/b.ts\n');
|
|
128
174
|
const { getChangedFiles } = await import('../ui-detector');
|
|
129
175
|
const files = getChangedFiles('main');
|
|
130
176
|
expect(files).toEqual(['src/a.ts', 'src/b.ts']);
|
|
131
177
|
});
|
|
132
178
|
|
|
133
179
|
it('returns empty array for empty diff', async () => {
|
|
134
|
-
|
|
180
|
+
mockSpawnReturn('');
|
|
135
181
|
const { getChangedFiles } = await import('../ui-detector');
|
|
136
182
|
const files = getChangedFiles('main');
|
|
137
183
|
expect(files).toEqual([]);
|
|
138
184
|
});
|
|
139
185
|
|
|
140
186
|
it('filters out empty lines', async () => {
|
|
141
|
-
|
|
187
|
+
mockSpawnReturn('src/a.ts\n\nsrc/b.ts\n');
|
|
142
188
|
const { getChangedFiles } = await import('../ui-detector');
|
|
143
189
|
const files = getChangedFiles('main');
|
|
144
190
|
expect(files).toEqual(['src/a.ts', 'src/b.ts']);
|
package/lib/detect-deps.js
CHANGED
|
@@ -179,89 +179,91 @@ async function checkDeps(platform = 'opencode') {
|
|
|
179
179
|
* @param {string} [platform='opencode'] - AI agent platform
|
|
180
180
|
* @returns {Promise<{ok: boolean, installed?: string[], errors?: Array<{name: string, message: string}>}>}
|
|
181
181
|
*/
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
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
|
+
}
|
|
185
193
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
};
|
|
194
|
-
}
|
|
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}` };
|
|
195
201
|
}
|
|
202
|
+
}
|
|
196
203
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
210
|
|
|
202
|
-
|
|
203
|
-
const
|
|
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
|
+
}
|
|
204
217
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
209
225
|
}
|
|
226
|
+
}
|
|
210
227
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 } };
|
|
221
241
|
}
|
|
242
|
+
}
|
|
222
243
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
for (const baseDir of skillsDirs) {
|
|
227
|
-
if (fs.existsSync(path.join(baseDir, dep.name))) {
|
|
228
|
-
exists = true;
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (exists) continue;
|
|
244
|
+
async function autoInstallDeps(platform = 'opencode') {
|
|
245
|
+
const skillsDirs = getSkillsDirs(platform);
|
|
246
|
+
const targetDir = skillsDirs[0];
|
|
233
247
|
|
|
234
|
-
|
|
235
|
-
|
|
248
|
+
const mkdirErr = ensureTargetDir(targetDir);
|
|
249
|
+
if (mkdirErr) return { ok: false, errors: [mkdirErr] };
|
|
236
250
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
encoding: 'utf8',
|
|
242
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
243
|
-
timeout: 120000, // 2 minute timeout
|
|
244
|
-
});
|
|
245
|
-
installed.push(dep.name);
|
|
246
|
-
console.log(` ${dep.name}: OK`);
|
|
247
|
-
} catch (e) {
|
|
248
|
-
errors.push({ name: dep.name, message: e.message || 'git clone failed' });
|
|
249
|
-
console.warn(` ${dep.name}: FAILED (${e.message || 'unknown error'})`);
|
|
250
|
-
// Clean up partial clone
|
|
251
|
-
try {
|
|
252
|
-
if (fs.existsSync(destPath)) {
|
|
253
|
-
fs.rmSync(destPath, { recursive: true, force: true });
|
|
254
|
-
}
|
|
255
|
-
} catch {
|
|
256
|
-
// Ignore cleanup failures
|
|
257
|
-
}
|
|
258
|
-
}
|
|
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)] };
|
|
259
255
|
}
|
|
260
256
|
|
|
261
|
-
|
|
262
|
-
|
|
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);
|
|
263
264
|
}
|
|
264
265
|
|
|
266
|
+
if (errors.length > 0) return { ok: false, installed, errors };
|
|
265
267
|
return { ok: true, installed };
|
|
266
268
|
}
|
|
267
269
|
|
package/lib/ui-detector.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
2
|
import { readFileSync, existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
@@ -75,6 +75,19 @@ function getFullExclusions(repoRoot?: string): string[] {
|
|
|
75
75
|
return [...UI_GATE_DEFAULT_EXCLUSIONS, ...loadUiGateIgnore(repoRoot)];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function runGitDiff(baseBranch: string): string {
|
|
79
|
+
const result = spawnSync(
|
|
80
|
+
'git',
|
|
81
|
+
['diff', '--name-only', `${baseBranch}..HEAD`],
|
|
82
|
+
{ stdio: ['pipe', 'pipe', 'pipe'], shell: false, encoding: 'utf8' }
|
|
83
|
+
);
|
|
84
|
+
if (result.status !== 0) {
|
|
85
|
+
const stderr = String(result.stderr ?? '').trim();
|
|
86
|
+
throw new Error(stderr || `git diff exited with status ${result.status}`);
|
|
87
|
+
}
|
|
88
|
+
return String(result.stdout ?? '').trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
export function detectUiSprint(baseBranch: string = 'main'): UiDetectionResult {
|
|
79
92
|
try {
|
|
80
93
|
const files = getChangedFiles(baseBranch);
|
|
@@ -88,18 +101,11 @@ export function detectUiSprint(baseBranch: string = 'main'): UiDetectionResult {
|
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
export function getChangedFiles(baseBranch: string): string[] {
|
|
91
|
-
const diffOutput =
|
|
92
|
-
|
|
93
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
94
|
-
}).trim();
|
|
95
|
-
|
|
96
|
-
if (diffOutput === '') {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
|
|
104
|
+
const diffOutput = runGitDiff(baseBranch);
|
|
105
|
+
if (diffOutput === '') return [];
|
|
100
106
|
return diffOutput
|
|
101
107
|
.split('\n')
|
|
102
|
-
.filter((f) => f.length > 0)
|
|
108
|
+
.filter((f: string) => f.length > 0)
|
|
103
109
|
.map(parseRenamedFile);
|
|
104
110
|
}
|
|
105
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 6 quality gates, Sprint Flow, and Delphi multi-expert review.",
|
|
6
6
|
"author": {
|