@boyingliu01/xp-gate 0.9.3 → 0.9.4
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__/baseline.test.js +89 -0
- package/lib/__tests__/doctor-helpers.test.js +139 -0
- package/lib/__tests__/upgrade-exec.test.js +42 -33
- package/lib/baseline.js +75 -48
- package/lib/doctor.js +180 -93
- package/lib/install-skill.js +81 -62
- package/lib/shared-phase-constants.d.ts +17 -0
- package/lib/shared-phase-constants.js +63 -0
- package/lib/shared-utils.js +14 -3
- package/lib/sprint-status.js +75 -83
- package/lib/upgrade.js +128 -85
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/__tests__/xp-gate-update.test.ts +133 -4
- package/plugins/opencode/index.ts +28 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/tui-plugin.ts +44 -75
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/lib/doctor.js
CHANGED
|
@@ -211,7 +211,32 @@ function diagnose() {
|
|
|
211
211
|
issues += configResult.issues;
|
|
212
212
|
|
|
213
213
|
// --- Check 2: Version mismatch ---
|
|
214
|
-
|
|
214
|
+
issues += diagnoseVersion(config, getPackageVersion(), checks);
|
|
215
|
+
|
|
216
|
+
// --- Check 3: templateDir validation ---
|
|
217
|
+
issues += diagnoseTemplateDir(config, checks);
|
|
218
|
+
|
|
219
|
+
// --- Check 4: Hooks files ---
|
|
220
|
+
if (config.mode === 'local') {
|
|
221
|
+
issues += checkLocalHooks(checks);
|
|
222
|
+
} else {
|
|
223
|
+
issues += checkGlobalHooks(checks);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- Check 5: Adapters directory ---
|
|
227
|
+
issues += checkAdapters(checks, config.mode, getGitDir());
|
|
228
|
+
|
|
229
|
+
// --- Check 6: Environment dependencies ---
|
|
230
|
+
checkEnv(checks);
|
|
231
|
+
|
|
232
|
+
return { checks, issues };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check 2: Version mismatch between config and package.
|
|
237
|
+
* @returns {number} issue count (0 or 1)
|
|
238
|
+
*/
|
|
239
|
+
function diagnoseVersion(config, pkgVersion, checks) {
|
|
215
240
|
const configVersion = config.version;
|
|
216
241
|
if (configVersion && pkgVersion && configVersion !== pkgVersion) {
|
|
217
242
|
checks.push({
|
|
@@ -219,10 +244,16 @@ function diagnose() {
|
|
|
219
244
|
status: 'FAIL',
|
|
220
245
|
detail: `config: ${configVersion}, package: ${pkgVersion} — run 'xp-gate doctor --fix' to sync`
|
|
221
246
|
});
|
|
222
|
-
|
|
247
|
+
return 1;
|
|
223
248
|
}
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
224
251
|
|
|
225
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Check 3: templateDir validation against platform expectation.
|
|
254
|
+
* @returns {number} issue count (0 or 1)
|
|
255
|
+
*/
|
|
256
|
+
function diagnoseTemplateDir(config, checks) {
|
|
226
257
|
const configTemplateDir = config.templateDir;
|
|
227
258
|
if (configTemplateDir) {
|
|
228
259
|
const expectedTemplateDir = getTemplateDir();
|
|
@@ -232,24 +263,10 @@ function diagnose() {
|
|
|
232
263
|
status: 'FAIL',
|
|
233
264
|
detail: `points to ${configTemplateDir}, expected ${expectedTemplateDir} for current platform`
|
|
234
265
|
});
|
|
235
|
-
|
|
266
|
+
return 1;
|
|
236
267
|
}
|
|
237
268
|
}
|
|
238
|
-
|
|
239
|
-
// --- Check 4: Hooks files ---
|
|
240
|
-
if (config.mode === 'local') {
|
|
241
|
-
issues += checkLocalHooks(checks);
|
|
242
|
-
} else {
|
|
243
|
-
issues += checkGlobalHooks(checks);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// --- Check 5: Adapters directory ---
|
|
247
|
-
issues += checkAdapters(checks, config.mode, getGitDir());
|
|
248
|
-
|
|
249
|
-
// --- Check 6: Environment dependencies ---
|
|
250
|
-
checkEnv(checks);
|
|
251
|
-
|
|
252
|
-
return { checks, issues };
|
|
269
|
+
return 0;
|
|
253
270
|
}
|
|
254
271
|
|
|
255
272
|
/**
|
|
@@ -280,82 +297,131 @@ function restoreHook(srcFile, destFile, label) {
|
|
|
280
297
|
}
|
|
281
298
|
|
|
282
299
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
300
|
+
* Fix version mismatch — update config.version to match package version.
|
|
301
|
+
* @param {object} config
|
|
302
|
+
* @param {string|null} pkgVersion
|
|
303
|
+
* @returns {boolean} Whether a fix was applied
|
|
285
304
|
*/
|
|
286
|
-
function
|
|
287
|
-
console.log('');
|
|
288
|
-
console.log('Attempting fixes...');
|
|
289
|
-
console.log('-------------------');
|
|
290
|
-
|
|
291
|
-
const srcDir = PKG_DIR;
|
|
292
|
-
let fixed = false;
|
|
293
|
-
|
|
294
|
-
// Fix version mismatch — update config version to match package
|
|
295
|
-
const pkgVersion = getPackageVersion();
|
|
305
|
+
function fixVersionMismatch(config, pkgVersion) {
|
|
296
306
|
if (pkgVersion && config.version !== pkgVersion) {
|
|
297
307
|
config.version = pkgVersion;
|
|
298
308
|
saveConfig(config);
|
|
299
309
|
console.log(` ✓ Updated config version to ${pkgVersion}`);
|
|
300
|
-
|
|
310
|
+
return true;
|
|
301
311
|
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
302
314
|
|
|
303
|
-
|
|
304
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Fix templateDir mismatch — update config.templateDir to match expected.
|
|
317
|
+
* @param {object} config
|
|
318
|
+
* @param {string} expectedTemplateDir
|
|
319
|
+
* @returns {boolean} Whether a fix was applied
|
|
320
|
+
*/
|
|
321
|
+
function fixTemplateDirMismatch(config, expectedTemplateDir) {
|
|
305
322
|
if (config.templateDir && config.templateDir !== expectedTemplateDir) {
|
|
306
323
|
config.templateDir = expectedTemplateDir;
|
|
307
324
|
saveConfig(config);
|
|
308
325
|
console.log(` ✓ Updated templateDir to ${expectedTemplateDir}`);
|
|
309
|
-
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Restore missing hooks from package source.
|
|
333
|
+
* @param {'local'|'global'} mode
|
|
334
|
+
* @param {string} srcDir - Package source directory
|
|
335
|
+
* @param {string} hooksDir - Target hooks directory
|
|
336
|
+
* @returns {boolean} Whether any hooks were restored
|
|
337
|
+
*/
|
|
338
|
+
function fixMissingHooks(mode, srcDir, hooksDir) {
|
|
339
|
+
let fixed = false;
|
|
340
|
+
const label = mode === 'local' ? '' : 'global ';
|
|
341
|
+
const preCommitLabel = `${label}pre-commit hook`;
|
|
342
|
+
const prePushLabel = `${label}pre-push hook`;
|
|
343
|
+
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-commit'), path.join(hooksDir, 'pre-commit'), preCommitLabel) || fixed;
|
|
344
|
+
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-push'), path.join(hooksDir, 'pre-push'), prePushLabel) || fixed;
|
|
345
|
+
return fixed;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Fix core.hooksPath for global mode.
|
|
350
|
+
* @param {string} globalHooksDir
|
|
351
|
+
* @returns {boolean} Whether the fix was applied
|
|
352
|
+
*/
|
|
353
|
+
function fixCoreHooksPath(globalHooksDir) {
|
|
354
|
+
try {
|
|
355
|
+
execSync(`git config --global core.hooksPath "${globalHooksDir}"`, {
|
|
356
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
357
|
+
});
|
|
358
|
+
console.log(` ✓ Set core.hooksPath to ${globalHooksDir}`);
|
|
359
|
+
return true;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.log(` ✗ Could not set core.hooksPath: ${e.message}`);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Restore missing adapters from package source.
|
|
368
|
+
* @param {'local'|'global'} mode
|
|
369
|
+
* @param {string} srcDir - Package source directory
|
|
370
|
+
* @param {string} adaptersDir - Target adapters directory
|
|
371
|
+
* @returns {boolean} Whether adapters were restored
|
|
372
|
+
*/
|
|
373
|
+
function fixMissingAdapters(mode, srcDir, adaptersDir) {
|
|
374
|
+
if (adaptersDir && (!fs.existsSync(adaptersDir) || fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh')).length === 0)) {
|
|
375
|
+
const pkgAdaptersDir = path.join(srcDir, 'adapters');
|
|
376
|
+
if (fs.existsSync(pkgAdaptersDir)) {
|
|
377
|
+
fs.mkdirSync(adaptersDir, { recursive: true });
|
|
378
|
+
const adapterFiles = fs.readdirSync(pkgAdaptersDir).filter(f => f.endsWith('.sh'));
|
|
379
|
+
for (const f of adapterFiles) {
|
|
380
|
+
fs.copyFileSync(path.join(pkgAdaptersDir, f), path.join(adaptersDir, f));
|
|
381
|
+
}
|
|
382
|
+
console.log(` ✓ Restored ${adapterFiles.length} adapter(s)`);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
310
385
|
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Attempt to fix known issues.
|
|
391
|
+
* Only operates when mode === 'active' (local or global).
|
|
392
|
+
*/
|
|
393
|
+
function fixIssues(checks, config) {
|
|
394
|
+
console.log('');
|
|
395
|
+
console.log('Attempting fixes...');
|
|
396
|
+
console.log('-------------------');
|
|
397
|
+
|
|
398
|
+
const srcDir = PKG_DIR;
|
|
399
|
+
let fixed = false;
|
|
400
|
+
|
|
401
|
+
fixed = fixVersionMismatch(config, getPackageVersion()) || fixed;
|
|
402
|
+
fixed = fixTemplateDirMismatch(config, getTemplateDir()) || fixed;
|
|
311
403
|
|
|
312
|
-
// Fix missing hooks
|
|
313
404
|
if (config.mode === 'local') {
|
|
314
405
|
const gitDir = getGitDir();
|
|
315
406
|
if (gitDir) {
|
|
316
407
|
const hooksDir = path.join(gitDir, 'hooks');
|
|
317
|
-
fixed =
|
|
318
|
-
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-push'), path.join(hooksDir, 'pre-push'), 'pre-push hook') || fixed;
|
|
408
|
+
fixed = fixMissingHooks('local', srcDir, hooksDir) || fixed;
|
|
319
409
|
}
|
|
320
410
|
} else {
|
|
321
|
-
|
|
322
|
-
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-commit'), path.join(hooksDir, 'pre-commit'), 'global pre-commit hook') || fixed;
|
|
323
|
-
fixed = restoreHook(path.join(srcDir, 'hooks', 'pre-push'), path.join(hooksDir, 'pre-push'), 'global pre-push hook') || fixed;
|
|
411
|
+
fixed = fixMissingHooks('global', srcDir, GLOBAL_HOOKS_DIR) || fixed;
|
|
324
412
|
}
|
|
325
413
|
|
|
326
|
-
// Fix core.hooksPath (global mode only)
|
|
327
414
|
if (config.mode === 'global') {
|
|
328
415
|
const hooksPath = getCurrentHooksPath();
|
|
329
416
|
if (hooksPath !== GLOBAL_HOOKS_DIR) {
|
|
330
|
-
|
|
331
|
-
execSync(`git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"`, {
|
|
332
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
333
|
-
});
|
|
334
|
-
console.log(` ✓ Set core.hooksPath to ${GLOBAL_HOOKS_DIR}`);
|
|
335
|
-
fixed = true;
|
|
336
|
-
} catch (e) {
|
|
337
|
-
console.log(` ✗ Could not set core.hooksPath: ${e.message}`);
|
|
338
|
-
}
|
|
417
|
+
fixed = fixCoreHooksPath(GLOBAL_HOOKS_DIR) || fixed;
|
|
339
418
|
}
|
|
340
419
|
}
|
|
341
420
|
|
|
342
|
-
// Fix missing adapters
|
|
343
421
|
const adaptersDir = config.mode === 'local'
|
|
344
422
|
? path.join(path.dirname(getGitDir() || ''), 'githooks', 'adapters')
|
|
345
423
|
: GLOBAL_ADAPTERS_DIR;
|
|
346
|
-
|
|
347
|
-
if (adaptersDir && (!fs.existsSync(adaptersDir) || fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh')).length === 0)) {
|
|
348
|
-
const pkgAdaptersDir = path.join(srcDir, 'adapters');
|
|
349
|
-
if (fs.existsSync(pkgAdaptersDir)) {
|
|
350
|
-
fs.mkdirSync(adaptersDir, { recursive: true });
|
|
351
|
-
const adapterFiles = fs.readdirSync(pkgAdaptersDir).filter(f => f.endsWith('.sh'));
|
|
352
|
-
for (const f of adapterFiles) {
|
|
353
|
-
fs.copyFileSync(path.join(pkgAdaptersDir, f), path.join(adaptersDir, f));
|
|
354
|
-
}
|
|
355
|
-
console.log(` ✓ Restored ${adapterFiles.length} adapter(s)`);
|
|
356
|
-
fixed = true;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
424
|
+
fixed = fixMissingAdapters(config.mode, srcDir, adaptersDir) || fixed;
|
|
359
425
|
|
|
360
426
|
if (!fixed) {
|
|
361
427
|
console.log(' No fixable issues found.');
|
|
@@ -374,31 +440,10 @@ function isUninstalledMode(config) {
|
|
|
374
440
|
return config && config !== 'corrupt' && config.mode === 'uninstalled';
|
|
375
441
|
}
|
|
376
442
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
console.log('==============');
|
|
382
|
-
|
|
383
|
-
const config = getConfig();
|
|
384
|
-
|
|
385
|
-
// §4.8: mode === "uninstalled" → print "xp-gate is not installed"
|
|
386
|
-
if (isUninstalledMode(config)) {
|
|
387
|
-
console.log('xp-gate is not installed.');
|
|
388
|
-
console.log('Run xp-gate init to install.');
|
|
389
|
-
return 0;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// §4.13: --fix only when mode === "active"
|
|
393
|
-
if (fixMode && isActiveMode(config)) {
|
|
394
|
-
fixIssues(null, config);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const { checks, issues } = diagnose();
|
|
398
|
-
|
|
399
|
-
printReport(checks);
|
|
400
|
-
|
|
401
|
-
// --- Check 7: Version upgrade check ---
|
|
443
|
+
/**
|
|
444
|
+
* Check 7: Version upgrade check (non-blocking).
|
|
445
|
+
*/
|
|
446
|
+
async function diagnoseUpgrade() {
|
|
402
447
|
try {
|
|
403
448
|
const upgradeResult = await checkUpgrade();
|
|
404
449
|
const msg = formatUpgradeMsg(upgradeResult, 'doctor');
|
|
@@ -406,8 +451,13 @@ async function doctor(args) {
|
|
|
406
451
|
console.log(`\n ℹ ${msg}`);
|
|
407
452
|
}
|
|
408
453
|
} catch { /* non-blocking — don't fail doctor on network issue */ }
|
|
454
|
+
}
|
|
409
455
|
|
|
410
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Check 8: OpenCode plugin version check.
|
|
458
|
+
* @returns {number} issue count
|
|
459
|
+
*/
|
|
460
|
+
function diagnoseOpenCodePlugin(checks) {
|
|
411
461
|
const pluginPath = path.join(HOME_DIR, '.config', 'opencode', 'node_modules', '@boyingliu01', 'opencode-plugin', 'package.json');
|
|
412
462
|
if (fs.existsSync(pluginPath)) {
|
|
413
463
|
try {
|
|
@@ -423,13 +473,43 @@ async function doctor(args) {
|
|
|
423
473
|
status: 'WARN',
|
|
424
474
|
detail: `plugin: ${pluginVersion}, xp-gate CLI: ${pkgVersion} — run 'cd ~/.config/opencode && npm update @boyingliu01/opencode-plugin'`
|
|
425
475
|
});
|
|
426
|
-
|
|
476
|
+
return 1;
|
|
427
477
|
}
|
|
428
478
|
}
|
|
429
479
|
} catch { /* skip */ }
|
|
430
480
|
} else {
|
|
431
481
|
checks.push({ name: 'OpenCode plugin', status: 'SKIP', detail: 'Not installed in OpenCode config' });
|
|
432
482
|
}
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function doctor(args) {
|
|
487
|
+
const fixMode = args.includes('--fix');
|
|
488
|
+
|
|
489
|
+
console.log('XP-Gate Doctor');
|
|
490
|
+
console.log('==============');
|
|
491
|
+
|
|
492
|
+
const config = getConfig();
|
|
493
|
+
|
|
494
|
+
// §4.8: mode === "uninstalled" → print "xp-gate is not installed"
|
|
495
|
+
if (isUninstalledMode(config)) {
|
|
496
|
+
console.log('xp-gate is not installed.');
|
|
497
|
+
console.log('Run xp-gate init to install.');
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// §4.13: --fix only when mode === "active"
|
|
502
|
+
if (fixMode && isActiveMode(config)) {
|
|
503
|
+
fixIssues(null, config);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const { checks, issues: diagnosedIssues } = diagnose();
|
|
507
|
+
let issues = diagnosedIssues;
|
|
508
|
+
|
|
509
|
+
printReport(checks);
|
|
510
|
+
|
|
511
|
+
await diagnoseUpgrade();
|
|
512
|
+
issues += diagnoseOpenCodePlugin(checks);
|
|
433
513
|
|
|
434
514
|
if (issues === 0) {
|
|
435
515
|
console.log('\n✓ All checks passed');
|
|
@@ -448,4 +528,11 @@ async function doctor(args) {
|
|
|
448
528
|
return issues > 0 ? 1 : 0;
|
|
449
529
|
}
|
|
450
530
|
|
|
451
|
-
module.exports = {
|
|
531
|
+
module.exports = {
|
|
532
|
+
doctor, isXpGateFile, SIGNATURES,
|
|
533
|
+
fixVersionMismatch,
|
|
534
|
+
fixTemplateDirMismatch,
|
|
535
|
+
fixMissingHooks,
|
|
536
|
+
fixCoreHooksPath,
|
|
537
|
+
fixMissingAdapters,
|
|
538
|
+
};
|
package/lib/install-skill.js
CHANGED
|
@@ -7,6 +7,7 @@ const { checkDeps } = require('./detect-deps.js');
|
|
|
7
7
|
const { downloadFromGitHub } = require('./download-skill.js');
|
|
8
8
|
const { rollback } = require('./rollback.js');
|
|
9
9
|
const { HOME_DIR, CONFIG_DIR, detectPlatform } = require('./shared-paths.js');
|
|
10
|
+
const { copyDirRecursive } = require('./shared-utils');
|
|
10
11
|
|
|
11
12
|
function getSkillsDir() {
|
|
12
13
|
const platform = detectPlatform();
|
|
@@ -28,7 +29,7 @@ const SKILLS_REGISTRY = {
|
|
|
28
29
|
|
|
29
30
|
async function installSkill(name, options = {}) {
|
|
30
31
|
const { offline = false, verbose = false, force = false } = options;
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
const platform = detectPlatform();
|
|
33
34
|
const depCheck = await checkDeps(platform);
|
|
34
35
|
if (!depCheck.ok) {
|
|
@@ -44,84 +45,104 @@ async function installSkill(name, options = {}) {
|
|
|
44
45
|
return 1;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
const skillInfo = validateSkillRegistry(name);
|
|
50
|
+
if (!skillInfo) return 1;
|
|
51
|
+
|
|
52
|
+
const targetDir = path.join(getSkillsDir(), name);
|
|
53
|
+
const dupError = checkDuplicateInstall(targetDir, force);
|
|
54
|
+
if (dupError) {
|
|
55
|
+
console.error(dupError);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const installId = `${name}-${Date.now()}`;
|
|
60
|
+
const backupDir = path.join(CONFIG_DIR, 'backup', installId);
|
|
61
|
+
backupExisting(targetDir, installId, backupDir);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await performInstall(skillInfo, name, targetDir, offline, verbose);
|
|
65
|
+
if (result !== 0) return result;
|
|
66
|
+
return 0;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`Error: Install failed - ${err.message}`);
|
|
69
|
+
await rollback(installId);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateSkillRegistry(name) {
|
|
48
75
|
const skillInfo = SKILLS_REGISTRY[name];
|
|
49
76
|
if (!skillInfo) {
|
|
50
77
|
console.error(`Error: Unknown skill: ${name}`);
|
|
51
78
|
console.error('Available skills: ' + Object.keys(SKILLS_REGISTRY).join(', '));
|
|
52
|
-
return
|
|
79
|
+
return null;
|
|
53
80
|
}
|
|
54
|
-
|
|
55
|
-
|
|
81
|
+
return skillInfo;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkDuplicateInstall(targetDir, force) {
|
|
56
85
|
if (fs.existsSync(targetDir) && !force) {
|
|
57
|
-
|
|
58
|
-
console.error('Use --force to overwrite');
|
|
59
|
-
return 1;
|
|
86
|
+
return `Error: ${path.basename(targetDir)} is already installed\nUse --force to overwrite`;
|
|
60
87
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function backupExisting(targetDir, installId, backupDir) {
|
|
65
92
|
if (fs.existsSync(targetDir)) {
|
|
66
93
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
67
94
|
copyDirRecursive(targetDir, backupDir);
|
|
68
95
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function performInstall(skillInfo, name, targetDir, offline, verbose) {
|
|
99
|
+
console.log(`Installing ${name}...`);
|
|
100
|
+
|
|
101
|
+
const skillUrl = `https://raw.githubusercontent.com/${skillInfo.repo}/main/${skillInfo.path}/SKILL.md`;
|
|
102
|
+
const targetFile = path.join(targetDir, 'SKILL.md');
|
|
103
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
104
|
+
|
|
105
|
+
let downloaded = false;
|
|
106
|
+
if (!offline) {
|
|
107
|
+
try {
|
|
108
|
+
await downloadFile(skillUrl, targetFile, verbose);
|
|
109
|
+
downloaded = true;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (verbose) console.warn(`Download failed: ${err.message}`);
|
|
86
112
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.error(`Error: Failed to download ${name}`);
|
|
94
|
-
console.error('Check network connection');
|
|
95
|
-
return 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!downloaded) {
|
|
116
|
+
if (offline) {
|
|
117
|
+
console.error(`Error: --offline specified but ${name} not in cache`);
|
|
118
|
+
return 2;
|
|
96
119
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
updateConfig({
|
|
100
|
-
installedSkills: {
|
|
101
|
-
...(getConfig().installedSkills || {}),
|
|
102
|
-
[name]: { version: '1.0.0', installedAt: new Date().toISOString() }
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (verbose) console.log(`Installed to ${targetDir}`);
|
|
107
|
-
console.log(`✓ ${name} installed`);
|
|
108
|
-
|
|
109
|
-
return 0;
|
|
110
|
-
} catch (err) {
|
|
111
|
-
console.error(`Error: Install failed - ${err.message}`);
|
|
112
|
-
await rollback(installId);
|
|
120
|
+
console.error(`Error: Failed to download ${name}`);
|
|
121
|
+
console.error('Check network connection');
|
|
113
122
|
return 1;
|
|
114
123
|
}
|
|
124
|
+
|
|
125
|
+
ensureConfigDir();
|
|
126
|
+
updateConfig({
|
|
127
|
+
installedSkills: {
|
|
128
|
+
...(getConfig().installedSkills || {}),
|
|
129
|
+
[name]: { version: '1.0.0', installedAt: new Date().toISOString() }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (verbose) console.log(`Installed to ${targetDir}`);
|
|
134
|
+
console.log(`✓ ${name} installed`);
|
|
135
|
+
return 0;
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
async function downloadFile(url, dest, verbose) {
|
|
118
139
|
return new Promise((resolve, reject) => {
|
|
119
140
|
const file = fs.createWriteStream(dest);
|
|
120
|
-
|
|
141
|
+
|
|
121
142
|
const protocol = url.startsWith('https') ? https : http;
|
|
122
|
-
|
|
143
|
+
|
|
123
144
|
if (verbose) console.log(`Downloading ${url}...`);
|
|
124
|
-
|
|
145
|
+
|
|
125
146
|
protocol.get(url, { timeout: 30000 }, (response) => {
|
|
126
147
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
127
148
|
const redirectUrl = response.headers.location;
|
|
@@ -130,13 +151,13 @@ async function downloadFile(url, dest, verbose) {
|
|
|
130
151
|
downloadFile(redirectUrl, dest, verbose).then(resolve).catch(reject);
|
|
131
152
|
return;
|
|
132
153
|
}
|
|
133
|
-
|
|
154
|
+
|
|
134
155
|
if (response.statusCode !== 200) {
|
|
135
156
|
file.close();
|
|
136
157
|
reject(new Error(`HTTP ${response.statusCode}`));
|
|
137
158
|
return;
|
|
138
159
|
}
|
|
139
|
-
|
|
160
|
+
|
|
140
161
|
response.pipe(file);
|
|
141
162
|
file.on('finish', () => {
|
|
142
163
|
file.close();
|
|
@@ -150,8 +171,6 @@ async function downloadFile(url, dest, verbose) {
|
|
|
150
171
|
});
|
|
151
172
|
}
|
|
152
173
|
|
|
153
|
-
const { copyDirRecursive } = require('./shared-utils');
|
|
154
|
-
|
|
155
174
|
function ensureConfigDir() {
|
|
156
175
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
157
176
|
}
|
|
@@ -173,4 +192,4 @@ function updateConfig(updates) {
|
|
|
173
192
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
module.exports = { installSkill };
|
|
195
|
+
module.exports = { installSkill };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for shared-phase-constants (CommonJS module).
|
|
3
|
+
* Consumed by both sprint-status.js (CJS) and tui-plugin.ts (ESM via tsx).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PHASE_NAMES: Record<string, string>;
|
|
7
|
+
export const PHASE_ORDER: string[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Find the most recent timestamp across started_at and phase_history.
|
|
11
|
+
*/
|
|
12
|
+
export function getLatestTimestamp(state: object): number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if the sprint state is stale (>1h since last activity).
|
|
16
|
+
*/
|
|
17
|
+
export function isStale(state: object): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Sprint Flow phase constants.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for PHASE_NAMES and PHASE_ORDER, consumed by both
|
|
5
|
+
* the CLI (sprint-status.js, CommonJS) and the OpenCode TUI plugin
|
|
6
|
+
* (tui-plugin.ts, ESM via tsx).
|
|
7
|
+
*
|
|
8
|
+
* @module shared-phase-constants
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const PHASE_NAMES = {
|
|
12
|
+
'-1': 'ISOLATE',
|
|
13
|
+
'-0.5': 'AUTO-ESTIMATE',
|
|
14
|
+
'0': 'THINK',
|
|
15
|
+
'1': 'PLAN',
|
|
16
|
+
'2': 'BUILD',
|
|
17
|
+
'3': 'REVIEW',
|
|
18
|
+
'4': 'USER ACCEPT',
|
|
19
|
+
'5': 'FEEDBACK',
|
|
20
|
+
'6': 'SHIP',
|
|
21
|
+
'7': 'LAND',
|
|
22
|
+
'8': 'CLEANUP',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const PHASE_ORDER = ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find the most recent timestamp across started_at and phase_history.
|
|
29
|
+
* @param {object} state - Sprint state object
|
|
30
|
+
* @returns {number} Latest timestamp in ms, or 0 if none found
|
|
31
|
+
*/
|
|
32
|
+
function getLatestTimestamp(state) {
|
|
33
|
+
if (!state || !state.started_at) return 0;
|
|
34
|
+
const started = new Date(state.started_at).getTime();
|
|
35
|
+
if (isNaN(started)) return 0;
|
|
36
|
+
let latest = started;
|
|
37
|
+
if (Array.isArray(state.phase_history)) {
|
|
38
|
+
for (const ph of state.phase_history) {
|
|
39
|
+
if (ph.completed_at) {
|
|
40
|
+
const t = new Date(ph.completed_at).getTime();
|
|
41
|
+
if (!isNaN(t) && t > latest) latest = t;
|
|
42
|
+
}
|
|
43
|
+
if (ph.started_at) {
|
|
44
|
+
const t = new Date(ph.started_at).getTime();
|
|
45
|
+
if (!isNaN(t) && t > latest) latest = t;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return latest;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if the sprint state is stale (>1h since last activity).
|
|
54
|
+
* @param {object} state - Sprint state object
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isStale(state) {
|
|
58
|
+
if (!state || !state.started_at) return false;
|
|
59
|
+
const latest = getLatestTimestamp(state);
|
|
60
|
+
return latest > 0 && Date.now() - latest > 3600000;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { PHASE_NAMES, PHASE_ORDER, getLatestTimestamp, isStale };
|
package/lib/shared-utils.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* No module-level state — safe for tests that mock fs/path/os.
|
|
4
4
|
*/
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Recursively copy a directory.
|
|
@@ -12,8 +13,8 @@ function copyDirRecursive(src, dest) {
|
|
|
12
13
|
fs.mkdirSync(dest, { recursive: true });
|
|
13
14
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
14
15
|
for (const entry of entries) {
|
|
15
|
-
const srcPath =
|
|
16
|
-
const destPath =
|
|
16
|
+
const srcPath = path.join(src, entry.name);
|
|
17
|
+
const destPath = path.join(dest, entry.name);
|
|
17
18
|
if (entry.isDirectory()) {
|
|
18
19
|
copyDirRecursive(srcPath, destPath);
|
|
19
20
|
} else {
|
|
@@ -22,4 +23,14 @@ function copyDirRecursive(src, dest) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
function readXpGateConfig() {
|
|
27
|
+
const cfgPath = path.join(require('os').homedir(), '.xp-gate', 'config.json');
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(cfgPath)) return null;
|
|
30
|
+
return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { copyDirRecursive, readXpGateConfig };
|