@boyingliu01/xp-gate 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__tests__/detect-deps.test.js +188 -17
- package/lib/__tests__/doctor.test.js +1 -0
- package/lib/__tests__/init.test.js +15 -0
- package/lib/__tests__/install-skill.test.js +1 -0
- package/lib/__tests__/uninstall.test.js +1 -0
- package/lib/detect-deps.js +181 -27
- package/lib/doctor.js +7 -9
- package/lib/init.js +42 -13
- package/lib/install-skill.js +5 -8
- package/lib/shared-paths.js +30 -0
- package/lib/uninstall.js +8 -9
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/skills/sprint-flow/SKILL.md +12 -9
|
@@ -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 =
|
|
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
|
-
|
|
151
|
-
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', '
|
|
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
|
|
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,14 +194,187 @@ 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
|
});
|
|
205
|
+
|
|
206
|
+
// ── Platform-specific tests (Issue #128) ──
|
|
207
|
+
|
|
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
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
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
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
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
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
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
|
+
// Mock execSync to avoid actual git clone
|
|
346
|
+
const { execSync } = require('child_process');
|
|
347
|
+
vi.spyOn(require('child_process'), 'execSync').mockImplementation((cmd) => {
|
|
348
|
+
// Simulate successful clone by creating the target dir
|
|
349
|
+
const match = cmd.match(/"([^"]+)"$/);
|
|
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 '';
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const result = await autoInstallDeps();
|
|
358
|
+
// Should have attempted to install
|
|
359
|
+
expect(result.ok).toBe(true);
|
|
360
|
+
expect(result.installed).toContain('superpowers');
|
|
361
|
+
expect(result.installed).toContain('gstack');
|
|
362
|
+
|
|
363
|
+
vi.restoreAllMocks();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('returns errors when git clone fails', async () => {
|
|
367
|
+
const { autoInstallDeps } = require('../detect-deps');
|
|
368
|
+
vi.spyOn(require('child_process'), 'execSync').mockImplementation(() => {
|
|
369
|
+
throw new Error('git not found');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const result = await autoInstallDeps();
|
|
373
|
+
expect(result.ok).toBe(false);
|
|
374
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
375
|
+
expect(result.errors[0].message).toContain('git not found');
|
|
376
|
+
|
|
377
|
+
vi.restoreAllMocks();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
209
380
|
});
|
|
@@ -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(() => {});
|
|
@@ -24,6 +24,7 @@ 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(() => {});
|
|
@@ -102,6 +103,13 @@ describe('init', () => {
|
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
it('init([]) with missing superpowers warns Missing dependencies', async () => {
|
|
106
|
+
// Mock execSync to make auto-install fail immediately
|
|
107
|
+
vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
108
|
+
if (typeof cmd === 'string' && cmd.includes('git clone')) {
|
|
109
|
+
throw new Error('git clone not available in test');
|
|
110
|
+
}
|
|
111
|
+
return '';
|
|
112
|
+
});
|
|
105
113
|
const { init } = require('../init');
|
|
106
114
|
const result = await init([]);
|
|
107
115
|
expect(result).toBe(0);
|
|
@@ -110,6 +118,13 @@ describe('init', () => {
|
|
|
110
118
|
});
|
|
111
119
|
|
|
112
120
|
it('init([]) with versionMismatch warns version detail', async () => {
|
|
121
|
+
// Mock execSync to make auto-install fail immediately
|
|
122
|
+
vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
123
|
+
if (typeof cmd === 'string' && cmd.includes('git clone')) {
|
|
124
|
+
throw new Error('git clone not available in test');
|
|
125
|
+
}
|
|
126
|
+
return '';
|
|
127
|
+
});
|
|
113
128
|
// superpowers too old, gstack good
|
|
114
129
|
const sp = path.join(skillsDir(), 'superpowers');
|
|
115
130
|
fs.mkdirSync(sp, { recursive: true });
|
|
@@ -69,6 +69,7 @@ describe('install-skill', () => {
|
|
|
69
69
|
vi.resetModules();
|
|
70
70
|
delete require.cache[require.resolve('../install-skill')];
|
|
71
71
|
delete require.cache[require.resolve('../detect-deps')];
|
|
72
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
72
73
|
delete require.cache[require.resolve('../download-skill')];
|
|
73
74
|
delete require.cache[require.resolve('../rollback')];
|
|
74
75
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -27,6 +27,7 @@ describe('uninstall', () => {
|
|
|
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(() => {});
|
package/lib/detect-deps.js
CHANGED
|
@@ -3,20 +3,72 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
|
|
6
|
+
const { HOME_DIR } = require('./shared-paths.js');
|
|
7
|
+
|
|
8
|
+
const REQUIRED_DEPS = [
|
|
9
|
+
{ name: 'superpowers', repo: 'obra/superpowers', minVersion: '1.0.0' },
|
|
10
|
+
{ name: 'gstack', repo: 'garrytan/gstack', minVersion: '1.0.0' }
|
|
11
|
+
];
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
14
|
+
* Platform profiles — each AI agent platform has its own skills directory.
|
|
15
|
+
* All platforms require the same dependencies (superpowers, gstack).
|
|
16
|
+
*
|
|
17
|
+
* @covers Issue #128 Bug 1 — Qoder platform was excluded from dependency checks
|
|
10
18
|
*/
|
|
11
|
-
const
|
|
19
|
+
const PLATFORM_PROFILES = {
|
|
20
|
+
opencode: {
|
|
21
|
+
skillsDirs: [
|
|
22
|
+
path.join(HOME_DIR, '.config', 'opencode', 'skills'),
|
|
23
|
+
path.join(HOME_DIR, '.config', 'opencode'),
|
|
24
|
+
],
|
|
25
|
+
requiredDeps: REQUIRED_DEPS,
|
|
26
|
+
},
|
|
27
|
+
'claude-code': {
|
|
28
|
+
skillsDirs: [
|
|
29
|
+
path.join(HOME_DIR, '.claude', 'skills'),
|
|
30
|
+
path.join(HOME_DIR, '.claude'),
|
|
31
|
+
],
|
|
32
|
+
requiredDeps: REQUIRED_DEPS,
|
|
33
|
+
},
|
|
34
|
+
qoder: {
|
|
35
|
+
skillsDirs: [
|
|
36
|
+
path.join(HOME_DIR, '.qoder', 'skills'),
|
|
37
|
+
path.join(HOME_DIR, '.qoder'),
|
|
38
|
+
],
|
|
39
|
+
requiredDeps: REQUIRED_DEPS,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
12
42
|
|
|
13
|
-
|
|
14
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Detect which AI agent platform is currently in use.
|
|
45
|
+
* Checks for platform-specific directories in the user's home.
|
|
46
|
+
* Falls back to 'opencode' if no platform is detected.
|
|
47
|
+
*
|
|
48
|
+
* @returns {'opencode' | 'claude-code' | 'qoder'}
|
|
49
|
+
*/
|
|
50
|
+
function detectPlatform() {
|
|
51
|
+
// Check for Qoder-specific marker
|
|
52
|
+
if (fs.existsSync(path.join(HOME_DIR, '.qoder', 'skills'))) {
|
|
53
|
+
return 'qoder';
|
|
54
|
+
}
|
|
55
|
+
// Check for Claude Code-specific marker
|
|
56
|
+
if (fs.existsSync(path.join(HOME_DIR, '.claude', 'skills'))) {
|
|
57
|
+
return 'claude-code';
|
|
58
|
+
}
|
|
59
|
+
// Default to opencode (most common, backward compatible)
|
|
60
|
+
return 'opencode';
|
|
61
|
+
}
|
|
15
62
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
]
|
|
63
|
+
/**
|
|
64
|
+
* Get the skills directories for a given platform.
|
|
65
|
+
* @param {string} platform - 'opencode' | 'claude-code' | 'qoder'
|
|
66
|
+
* @returns {string[]}
|
|
67
|
+
*/
|
|
68
|
+
function getSkillsDirs(platform) {
|
|
69
|
+
const profile = PLATFORM_PROFILES[platform] || PLATFORM_PROFILES.opencode;
|
|
70
|
+
return profile.skillsDirs;
|
|
71
|
+
}
|
|
20
72
|
|
|
21
73
|
/**
|
|
22
74
|
* Check if bash is available on the system.
|
|
@@ -84,25 +136,28 @@ function checkBash() {
|
|
|
84
136
|
}
|
|
85
137
|
}
|
|
86
138
|
|
|
87
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Check if required dependencies are installed for the given platform.
|
|
141
|
+
* @param {string} [platform='opencode'] - AI agent platform
|
|
142
|
+
* @returns {Promise<{ok: boolean, missing?: string, versionMismatch?: object}>}
|
|
143
|
+
*/
|
|
144
|
+
async function checkDeps(platform = 'opencode') {
|
|
145
|
+
const skillsDirs = getSkillsDirs(platform);
|
|
146
|
+
|
|
88
147
|
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
148
|
let depDir = null;
|
|
95
|
-
for (const
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
for (const baseDir of skillsDirs) {
|
|
150
|
+
const candidate = path.join(baseDir, dep.name);
|
|
151
|
+
if (fs.existsSync(candidate)) {
|
|
152
|
+
depDir = candidate;
|
|
98
153
|
break;
|
|
99
154
|
}
|
|
100
155
|
}
|
|
101
|
-
|
|
156
|
+
|
|
102
157
|
if (!depDir) {
|
|
103
158
|
return { ok: false, missing: dep.name };
|
|
104
159
|
}
|
|
105
|
-
|
|
160
|
+
|
|
106
161
|
const version = await getSkillVersion(depDir);
|
|
107
162
|
if (version && compareVersions(version, dep.minVersion) < 0) {
|
|
108
163
|
return {
|
|
@@ -115,10 +170,101 @@ async function checkDeps() {
|
|
|
115
170
|
};
|
|
116
171
|
}
|
|
117
172
|
}
|
|
118
|
-
|
|
173
|
+
|
|
119
174
|
return { ok: true };
|
|
120
175
|
}
|
|
121
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Auto-install missing dependencies by cloning from GitHub.
|
|
179
|
+
* @param {string} [platform='opencode'] - AI agent platform
|
|
180
|
+
* @returns {Promise<{ok: boolean, installed?: string[], errors?: Array<{name: string, message: string}>}>}
|
|
181
|
+
*/
|
|
182
|
+
async function autoInstallDeps(platform = 'opencode') {
|
|
183
|
+
const skillsDirs = getSkillsDirs(platform);
|
|
184
|
+
const targetDir = skillsDirs[0]; // Install to the primary skills directory
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(targetDir)) {
|
|
187
|
+
try {
|
|
188
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
errors: [{ name: 'mkdir', message: `Cannot create skills directory: ${e.message}` }]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const check = await checkDeps(platform);
|
|
198
|
+
if (check.ok) {
|
|
199
|
+
return { ok: true, installed: [] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const errors = [];
|
|
203
|
+
const installed = [];
|
|
204
|
+
|
|
205
|
+
// Check each dep: missing → install, version mismatch → can't auto-fix
|
|
206
|
+
const depCheck = await checkDeps(platform);
|
|
207
|
+
if (depCheck.ok) {
|
|
208
|
+
return { ok: true, installed: [] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// If the issue is version mismatch, auto-install can't help
|
|
212
|
+
if (depCheck.versionMismatch) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
installed: [],
|
|
216
|
+
errors: [{
|
|
217
|
+
name: depCheck.versionMismatch.name,
|
|
218
|
+
message: `version mismatch: need ${depCheck.versionMismatch.required}, found ${depCheck.versionMismatch.found} (auto-install cannot upgrade)`
|
|
219
|
+
}]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const dep of REQUIRED_DEPS) {
|
|
224
|
+
// Re-check if this specific dep is missing
|
|
225
|
+
let exists = false;
|
|
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;
|
|
233
|
+
|
|
234
|
+
const destPath = path.join(targetDir, dep.name);
|
|
235
|
+
const repoUrl = `https://github.com/${dep.repo}.git`;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
console.log(` ${dep.name}: not found → installing from ${repoUrl} ...`);
|
|
239
|
+
const cp = require('child_process');
|
|
240
|
+
cp.execSync(`git clone --depth 1 "${repoUrl}" "${destPath}"`, {
|
|
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
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (errors.length > 0) {
|
|
262
|
+
return { ok: false, installed, errors };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { ok: true, installed };
|
|
266
|
+
}
|
|
267
|
+
|
|
122
268
|
async function getSkillVersion(skillDir) {
|
|
123
269
|
const pkgFile = path.join(skillDir, 'package.json');
|
|
124
270
|
if (fs.existsSync(pkgFile)) {
|
|
@@ -127,7 +273,7 @@ async function getSkillVersion(skillDir) {
|
|
|
127
273
|
return pkg.version;
|
|
128
274
|
} catch {}
|
|
129
275
|
}
|
|
130
|
-
|
|
276
|
+
|
|
131
277
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
132
278
|
if (fs.existsSync(skillFile)) {
|
|
133
279
|
const content = fs.readFileSync(skillFile, 'utf8');
|
|
@@ -136,22 +282,30 @@ async function getSkillVersion(skillDir) {
|
|
|
136
282
|
return versionMatch[1];
|
|
137
283
|
}
|
|
138
284
|
}
|
|
139
|
-
|
|
285
|
+
|
|
140
286
|
return null;
|
|
141
287
|
}
|
|
142
288
|
|
|
143
289
|
function compareVersions(a, b) {
|
|
144
290
|
const partsA = a.split('.').map(Number);
|
|
145
291
|
const partsB = b.split('.').map(Number);
|
|
146
|
-
|
|
292
|
+
|
|
147
293
|
for (let i = 0; i < 3; i++) {
|
|
148
294
|
const partA = partsA[i] || 0;
|
|
149
295
|
const partB = partsB[i] || 0;
|
|
150
296
|
if (partA > partB) return 1;
|
|
151
297
|
if (partA < partB) return -1;
|
|
152
298
|
}
|
|
153
|
-
|
|
299
|
+
|
|
154
300
|
return 0;
|
|
155
301
|
}
|
|
156
302
|
|
|
157
|
-
module.exports = {
|
|
303
|
+
module.exports = {
|
|
304
|
+
checkDeps,
|
|
305
|
+
checkBash,
|
|
306
|
+
autoInstallDeps,
|
|
307
|
+
detectPlatform,
|
|
308
|
+
getSkillsDirs,
|
|
309
|
+
PLATFORM_PROFILES,
|
|
310
|
+
REQUIRED_DEPS,
|
|
311
|
+
};
|
package/lib/doctor.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const { execSync } = require('child_process');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
|
|
4
|
+
const {
|
|
5
|
+
HOME_DIR,
|
|
6
|
+
CONFIG_DIR,
|
|
7
|
+
CONFIG_FILE,
|
|
8
|
+
GLOBAL_HOOKS_DIR,
|
|
9
|
+
GLOBAL_ADAPTERS_DIR,
|
|
10
|
+
} = require('./shared-paths.js');
|
|
13
11
|
|
|
14
12
|
// npm package source dir (template hooks/adapters)
|
|
15
13
|
const PKG_DIR = path.dirname(__dirname);
|
package/lib/init.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const crypto = require('crypto');
|
|
5
|
-
const { checkDeps } = require('./detect-deps.js');
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
|
|
15
|
-
const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
|
|
4
|
+
const { checkDeps, checkBash, autoInstallDeps, detectPlatform } = require('./detect-deps.js');
|
|
5
|
+
const {
|
|
6
|
+
HOME_DIR,
|
|
7
|
+
CONFIG_DIR,
|
|
8
|
+
CONFIG_FILE,
|
|
9
|
+
TEMPLATE_DIR,
|
|
10
|
+
GLOBAL_HOOKS_DIR,
|
|
11
|
+
GLOBAL_ADAPTERS_DIR,
|
|
12
|
+
} = require('./shared-paths.js');
|
|
16
13
|
|
|
17
14
|
function copyHooks(srcDir, destDir) {
|
|
18
15
|
['pre-commit', 'pre-push'].forEach(hook => {
|
|
@@ -54,6 +51,35 @@ function logDeps(depCheck) {
|
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Detect which AI agent platform is in use and check/auto-install deps.
|
|
56
|
+
* @param {string} platform - 'opencode' | 'claude-code' | 'qoder'
|
|
57
|
+
*/
|
|
58
|
+
async function checkAndInstallDeps(platform) {
|
|
59
|
+
const depCheck = await checkDeps(platform);
|
|
60
|
+
if (depCheck.ok) {
|
|
61
|
+
logDeps(depCheck);
|
|
62
|
+
return depCheck;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Auto-install missing deps
|
|
66
|
+
console.log(`Checking dependencies (platform: ${platform})...`);
|
|
67
|
+
const installResult = await autoInstallDeps(platform);
|
|
68
|
+
if (installResult.ok) {
|
|
69
|
+
console.log('Dependencies: OK (auto-installed)\n');
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Auto-install failed — report and continue with warning
|
|
74
|
+
logDeps(depCheck);
|
|
75
|
+
if (installResult.errors) {
|
|
76
|
+
for (const err of installResult.errors) {
|
|
77
|
+
console.warn(` Auto-install failed for ${err.name}: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return depCheck;
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
function printUsage() {
|
|
58
84
|
console.log('Choose installation mode:');
|
|
59
85
|
console.log(' 1) Global — all git projects use the same hooks (recommended)');
|
|
@@ -228,7 +254,10 @@ async function init(args) {
|
|
|
228
254
|
console.warn(` ${bashCheck.message}\n`);
|
|
229
255
|
}
|
|
230
256
|
|
|
231
|
-
|
|
257
|
+
// Detect platform and check/auto-install dependencies
|
|
258
|
+
const platform = detectPlatform();
|
|
259
|
+
console.log(`Platform: ${platform}\n`);
|
|
260
|
+
await checkAndInstallDeps(platform);
|
|
232
261
|
|
|
233
262
|
const installMode = args.includes('--global') ? 'global' :
|
|
234
263
|
args.includes('--core-only') ? 'local' :
|
package/lib/install-skill.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const https = require('https');
|
|
5
4
|
const http = require('http');
|
|
6
5
|
const { execSync } = require('child_process');
|
|
7
|
-
const { checkDeps } = require('./detect-deps.js');
|
|
6
|
+
const { checkDeps, detectPlatform } = require('./detect-deps.js');
|
|
8
7
|
const { downloadFromGitHub } = require('./download-skill.js');
|
|
9
8
|
const { rollback } = require('./rollback.js');
|
|
9
|
+
const { HOME_DIR, CONFIG_DIR } = require('./shared-paths.js');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
13
|
-
|
|
14
|
-
const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
|
|
15
|
-
const SKILLS_DIR = path.join(HOME, '.config', 'opencode', 'skills');
|
|
11
|
+
const SKILLS_DIR = path.join(HOME_DIR, '.config', 'opencode', 'skills');
|
|
16
12
|
|
|
17
13
|
const SKILLS_REGISTRY = {
|
|
18
14
|
'sprint-flow': { repo: 'boyingliu01/xp-gate', path: 'skills/sprint-flow' },
|
|
@@ -24,7 +20,8 @@ const SKILLS_REGISTRY = {
|
|
|
24
20
|
async function installSkill(name, options = {}) {
|
|
25
21
|
const { offline = false, verbose = false, force = false } = options;
|
|
26
22
|
|
|
27
|
-
const
|
|
23
|
+
const platform = detectPlatform();
|
|
24
|
+
const depCheck = await checkDeps(platform);
|
|
28
25
|
if (!depCheck.ok) {
|
|
29
26
|
if (depCheck.missing) {
|
|
30
27
|
console.error(`Error: ${depCheck.missing} is required but not installed`);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filesystem path constants used across CLI modules.
|
|
3
|
+
* Cross-platform home directory resolution with os.homedir() fallback.
|
|
4
|
+
*
|
|
5
|
+
* @intent Eliminate duplicate path constants in init.js / uninstall.js / detect-deps.js
|
|
6
|
+
* @covers Issue #107 — duplicate code between init.js and uninstall.js
|
|
7
|
+
*/
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the user's home directory cross-platform.
|
|
13
|
+
* Fallback chain: HOME → USERPROFILE → os.homedir()
|
|
14
|
+
*/
|
|
15
|
+
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
|
|
18
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
|
|
19
|
+
const TEMPLATE_DIR = path.join(HOME_DIR, '.config', 'opencode', 'git-hooks-template');
|
|
20
|
+
const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
|
|
21
|
+
const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
HOME_DIR,
|
|
25
|
+
CONFIG_DIR,
|
|
26
|
+
CONFIG_FILE,
|
|
27
|
+
TEMPLATE_DIR,
|
|
28
|
+
GLOBAL_HOOKS_DIR,
|
|
29
|
+
GLOBAL_ADAPTERS_DIR,
|
|
30
|
+
};
|
package/lib/uninstall.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const crypto = require('crypto');
|
|
4
|
+
const {
|
|
5
|
+
HOME_DIR,
|
|
6
|
+
CONFIG_DIR,
|
|
7
|
+
CONFIG_FILE,
|
|
8
|
+
TEMPLATE_DIR,
|
|
9
|
+
GLOBAL_HOOKS_DIR,
|
|
10
|
+
GLOBAL_ADAPTERS_DIR,
|
|
11
|
+
} = require('./shared-paths.js');
|
|
5
12
|
|
|
6
|
-
// Cross-platform home directory resolution
|
|
7
|
-
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
8
|
-
|
|
9
|
-
const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
|
|
10
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
|
|
11
|
-
const TEMPLATE_DIR = path.join(HOME_DIR, '.config', 'opencode', 'git-hooks-template');
|
|
12
|
-
const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
|
|
13
|
-
const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
|
|
14
13
|
const BACKUP_DIR = path.join(CONFIG_DIR, '.uninstall-backup');
|
|
15
14
|
|
|
16
15
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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": {
|
|
@@ -462,9 +462,12 @@ Phase 2 第一步必须执行 DELPHI-GATE 检查。没有 delphi-review APPROVED
|
|
|
462
462
|
- 输入: phase-4-summary.md(验收结果)+ emergent-issues.md(如有)
|
|
463
463
|
- 输出: `feedback-log.md`
|
|
464
464
|
- **HARD-GATE**: Phase 5 不可跳过。Phase 4 完成后 → 必须进入 Phase 5 → 完成后才能进入 Phase 6。
|
|
465
|
-
- **`learn` (gstack)** — Sprint
|
|
465
|
+
- **`learn` (gstack)** — Sprint 级复盘(**Phase 5 必须自动调用,不依赖手动触发**)
|
|
466
|
+
- **默认提炼模板**(无需用户额外输入):
|
|
467
|
+
> 提炼总结并保存可复用的经验教训,把大模型不知道、并且犯错后无法立即发现和纠正的知识保存下来。如果是对其他项目也有价值的,就保存成全局记忆,否则保存为项目记忆。
|
|
466
468
|
- ralph-loop 已在 BUILD Phase 内部实现 per-REQ learn(permanent/contextual 分类)
|
|
467
469
|
- Phase 5 额外进行 Sprint 级复盘,总结全 Phase 经验
|
|
470
|
+
- **learnings 自动注入**:`/learn export` 时自动 append 到 CLAUDE.md / AGENTS.md 末尾形成 `## Project Learnings` 章节
|
|
468
471
|
- **`retro` (gstack)** — 工程回顾:提交历史、工作模式、代码质量趋势
|
|
469
472
|
- **`systematic-debugging` (superpowers)** — 根因调试
|
|
470
473
|
|
|
@@ -620,10 +623,11 @@ Phase 2 第一步必须执行 DELPHI-GATE 检查。没有 delphi-review APPROVED
|
|
|
620
623
|
|
|
621
624
|
3. **等待用户确认 checkpoint**(如适用)
|
|
622
625
|
|
|
623
|
-
4.
|
|
626
|
+
4. **展示进度看板**:执行 `node scripts/render-sprint-progress.cjs` 渲染进度看板
|
|
627
|
+
- 脚本自动读取 `.sprint-state/sprint-state.json` 并输出 ASCII 进度看板
|
|
624
628
|
- 渲染规则:已完成阶段显示 ✅ + 耗时,当前阶段 🔄,待做 ⬜,跳过 ⏭️,失败 ❌
|
|
625
629
|
- 进度条:`[████▓░░░░░░] {pct}%`(已完成数/总阶段数 11)
|
|
626
|
-
- 下一步行动:根据当前阶段 +
|
|
630
|
+
- 下一步行动:根据当前阶段 + 状态,自动查找对应提示
|
|
627
631
|
- 输出物路径:列出 `outputs` 中已有的文件路径
|
|
628
632
|
- 时机:每个 Phase 完成后的 transition 阶段自动展示,用户无需请求
|
|
629
633
|
- 向后兼容:旧版 `sprint-state.json` 缺少 `phase_history` 时,从 `phase` 字段推断状态
|
|
@@ -792,17 +796,16 @@ Sprint state is persisted as JSON in `.sprint-state/sprint-state.json`:
|
|
|
792
796
|
|
|
793
797
|
```bash
|
|
794
798
|
/sprint-flow --status
|
|
795
|
-
# →
|
|
796
|
-
# →
|
|
799
|
+
# → 执行 node scripts/render-sprint-progress.cjs
|
|
800
|
+
# → 读取 .sprint-state/sprint-state.json 并渲染 ASCII 进度看板
|
|
797
801
|
# → 不执行任何 Phase,仅展示当前状态
|
|
798
802
|
# 适用场景:碎片时间恢复时快速查看进度、中断后确认当前阶段和下一步操作
|
|
799
803
|
```
|
|
800
804
|
|
|
801
805
|
**行为规则**:
|
|
802
|
-
-
|
|
803
|
-
-
|
|
804
|
-
- 如果 `
|
|
805
|
-
- 如果 `status == "completed"` → 展示完整看板 + `[INFO] Sprint 已完成。` + Sprint Summary 路径
|
|
806
|
+
- 执行 `node scripts/render-sprint-progress.cjs`(脚本自动处理所有渲染逻辑)
|
|
807
|
+
- 如果 `sprint-state.json` 不存在 → 脚本输出 `[INFO] 未找到活跃的 Sprint。请先运行 /sprint-flow "[需求描述]" 启动新 Sprint。`
|
|
808
|
+
- 如果 `status == "completed"` → 脚本输出完整看板 + `[INFO] Sprint 已完成。`
|
|
806
809
|
- `--status` 可与其他参数组合:`--status --resume-from build` → 先展示状态,再提示 "将从 Phase 2 BUILD 继续"
|
|
807
810
|
- 向后兼容:旧版 `sprint-state.json` 缺少 `phase_history`/`task_description` 时,按模板"向后兼容"规则渲染
|
|
808
811
|
|