@luanpdd/kit-mcp 1.5.4 → 1.6.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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/cli/index.js +179 -0
- package/src/cli/upgrade-check.js +135 -0
- package/src/core/gates.js +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.6.1] - 2026-05-05
|
|
10
|
+
|
|
11
|
+
DX patch: comando `kit doctor` + upgrade-check no boot do sidecar + cache de gates.
|
|
12
|
+
|
|
13
|
+
### Adicionado
|
|
14
|
+
|
|
15
|
+
- **`kit doctor`** ([src/cli/index.js](src/cli/index.js), nova função `runDoctorChecks`) — comando único de diagnóstico que checa: versão local vs npm latest, sidecar reachability via lockfile + healthz, validade do `~/.claude/settings.json`, presença do hook PostToolUse `sidecar-tool-publisher`, dirs do kit bundled, integridade do `.planning/`, e lockfiles órfãos em tmpdir. Retorna checklist colorido com `fix:` específico em cada falha. Suporta `--json` (via flag global) pra consumo programático.
|
|
16
|
+
- **Upgrade-check no `kit ui start`** ([src/cli/index.js](src/cli/index.js), [src/cli/upgrade-check.js](src/cli/upgrade-check.js)) — verifica npm registry em background; se versão local atrás da latest, imprime banner amarelo "v1.6 → v1.6.1 disponível, atualize com npm i -g". Cache TTL 24h em `~/.kit-mcp/version-check.json` evita hit no registry em todo boot. Falha gracefully em modo offline.
|
|
17
|
+
- **Cache TTL em `listGates`** ([src/core/gates.js](src/core/gates.js)) — mesmo padrão de PERF-01 (`listKit`). Sequência `listGates → getGate → gatesForStage` num único processo agora faz 1 walk de disco em vez de 3.
|
|
18
|
+
|
|
19
|
+
### Sem mudanças de API
|
|
20
|
+
|
|
21
|
+
`mcp__kit__gates` action=list e action=get continuam funcionando com mesma assinatura. Doctor e upgrade-check são CLI-only.
|
|
22
|
+
|
|
23
|
+
### Testes
|
|
24
|
+
|
|
25
|
+
+10 unit (112 total): 8 cobrindo `compareVersions`/`getLocalVersion`/constantes do upgrade-check, 2 cobrindo cache hit/miss em gates.
|
|
26
|
+
|
|
9
27
|
## [1.6.0] - 2026-05-05
|
|
10
28
|
|
|
11
29
|
Milestone v1.6 — perf+lean: 16 itens de auditoria de codebase entregues em 3 fases (Phase 19 quick wins, Phase 20 hardening, Phase 21 token economy) + observability hook (Phase 19.5).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luanpdd/kit-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/index.js
CHANGED
|
@@ -33,7 +33,10 @@ import { createServer } from '../ui/server.js';
|
|
|
33
33
|
import { readLock, lockPathFor } from '../ui/lockfile.js';
|
|
34
34
|
import { wrapProgressForUi } from '../ui/wrapper.js';
|
|
35
35
|
import { openBrowser } from '../ui/browser.js';
|
|
36
|
+
import { checkUpgrade, getLocalVersion } from './upgrade-check.js';
|
|
36
37
|
import http from 'node:http';
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
37
40
|
|
|
38
41
|
// Read package.json version at boot so `--version` is always accurate. Falls
|
|
39
42
|
// back to a string if the file lookup fails (e.g. unusual install layout).
|
|
@@ -391,6 +394,14 @@ ui.command('start')
|
|
|
391
394
|
const url = `http://127.0.0.1:${actualPort}/`;
|
|
392
395
|
process.stderr.write(`${c.cyan(icons.info)} kit-mcp ui listening on ${url}\n`);
|
|
393
396
|
process.stderr.write(`${c.dim(` project: ${projectRoot}`)}\n`);
|
|
397
|
+
// U4: non-blocking upgrade check. Warns if local install is behind npm latest.
|
|
398
|
+
// Cached for 24h via ~/.kit-mcp/version-check.json so we don't hit npm on every start.
|
|
399
|
+
checkUpgrade().then((info) => {
|
|
400
|
+
if (info?.behind) {
|
|
401
|
+
process.stderr.write(`${c.yellow(icons.warn)} kit-mcp v${info.local} → v${info.latest} disponível\n`);
|
|
402
|
+
process.stderr.write(`${c.dim(' atualize com: npm i -g @luanpdd/kit-mcp@latest')}\n`);
|
|
403
|
+
}
|
|
404
|
+
}).catch(() => { /* offline / silent */ });
|
|
394
405
|
if (opts.open !== false) {
|
|
395
406
|
await openBrowser(url);
|
|
396
407
|
}
|
|
@@ -457,6 +468,174 @@ ui.command('open')
|
|
|
457
468
|
}
|
|
458
469
|
});
|
|
459
470
|
|
|
471
|
+
// --- doctor (DX diagnostic) ---
|
|
472
|
+
program.command('doctor')
|
|
473
|
+
.description('Diagnose kit-mcp setup: version, sidecar, hook, settings.json, lockfile, .planning/.')
|
|
474
|
+
.option('--project-root <path>', 'Project to diagnose (default: cwd)')
|
|
475
|
+
.action(async (opts) => {
|
|
476
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
477
|
+
const checks = await runDoctorChecks(projectRoot);
|
|
478
|
+
const failed = checks.filter(c => c.status === 'fail').length;
|
|
479
|
+
const warned = checks.filter(c => c.status === 'warn').length;
|
|
480
|
+
|
|
481
|
+
if (program.opts().json) {
|
|
482
|
+
out({ checks, failed, warned }, () => '');
|
|
483
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
process.stdout.write(`\n${c.bold('kit-mcp doctor')} — ${projectRoot}\n\n`);
|
|
487
|
+
for (const check of checks) {
|
|
488
|
+
const sym = check.status === 'pass' ? c.green(icons.check)
|
|
489
|
+
: check.status === 'warn' ? c.yellow(icons.warn)
|
|
490
|
+
: c.red(icons.cross);
|
|
491
|
+
process.stdout.write(`${sym} ${c.bold(check.label)}\n`);
|
|
492
|
+
if (check.detail) process.stdout.write(` ${c.dim(check.detail)}\n`);
|
|
493
|
+
if (check.fix) process.stdout.write(` ${c.cyan('fix:')} ${check.fix}\n`);
|
|
494
|
+
}
|
|
495
|
+
process.stdout.write('\n');
|
|
496
|
+
if (failed > 0) {
|
|
497
|
+
process.stdout.write(`${c.red(icons.cross)} ${failed} check(s) failed\n`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
} else if (warned > 0) {
|
|
500
|
+
process.stdout.write(`${c.yellow(icons.warn)} ${warned} warning(s) — kit-mcp is functional\n`);
|
|
501
|
+
} else {
|
|
502
|
+
process.stdout.write(`${c.green(icons.check)} all checks passed\n`);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
async function runDoctorChecks(projectRoot) {
|
|
507
|
+
const checks = [];
|
|
508
|
+
|
|
509
|
+
// 1. Version + upgrade availability
|
|
510
|
+
const upgrade = await checkUpgrade();
|
|
511
|
+
if (!upgrade) {
|
|
512
|
+
checks.push({ label: 'version', status: 'fail',
|
|
513
|
+
detail: 'could not read local package.json',
|
|
514
|
+
fix: 'reinstall via `npm i -g @luanpdd/kit-mcp@latest`' });
|
|
515
|
+
} else if (upgrade.latest === null) {
|
|
516
|
+
checks.push({ label: 'version', status: 'warn',
|
|
517
|
+
detail: `local v${upgrade.local} (offline — could not check npm)` });
|
|
518
|
+
} else if (upgrade.behind) {
|
|
519
|
+
checks.push({ label: 'version', status: 'warn',
|
|
520
|
+
detail: `local v${upgrade.local}, latest v${upgrade.latest}`,
|
|
521
|
+
fix: 'npm i -g @luanpdd/kit-mcp@latest' });
|
|
522
|
+
} else {
|
|
523
|
+
checks.push({ label: 'version', status: 'pass',
|
|
524
|
+
detail: `v${upgrade.local} (latest)` });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 2. Sidecar lockfile + healthz
|
|
528
|
+
const lock = readLock(projectRoot);
|
|
529
|
+
if (!lock) {
|
|
530
|
+
checks.push({ label: 'sidecar', status: 'warn',
|
|
531
|
+
detail: 'not running for this project',
|
|
532
|
+
fix: 'kit ui start (or omit if you don\'t need the live viewer)' });
|
|
533
|
+
} else {
|
|
534
|
+
try {
|
|
535
|
+
await getHealthz(lock.port);
|
|
536
|
+
checks.push({ label: 'sidecar', status: 'pass',
|
|
537
|
+
detail: `running on port ${lock.port} (pid ${lock.pid})` });
|
|
538
|
+
} catch (err) {
|
|
539
|
+
checks.push({ label: 'sidecar', status: 'fail',
|
|
540
|
+
detail: `lockfile says port ${lock.port} but unreachable: ${err.message}`,
|
|
541
|
+
fix: 'kit ui stop && kit ui start' });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 3. ~/.claude/settings.json — exists + valid JSON + hooks present?
|
|
546
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
547
|
+
let settings = null;
|
|
548
|
+
try {
|
|
549
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
550
|
+
checks.push({ label: 'settings.json', status: 'pass',
|
|
551
|
+
detail: settingsPath });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (err.code === 'ENOENT') {
|
|
554
|
+
checks.push({ label: 'settings.json', status: 'warn',
|
|
555
|
+
detail: 'not found (expected for fresh Claude Code)',
|
|
556
|
+
fix: 'will be created automatically by Claude Code' });
|
|
557
|
+
} else {
|
|
558
|
+
checks.push({ label: 'settings.json', status: 'fail',
|
|
559
|
+
detail: `invalid JSON at ${settingsPath}: ${err.message}`,
|
|
560
|
+
fix: 'edit the file or restore from .claude/settings.json.bak' });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 4. Hook installed?
|
|
565
|
+
if (settings) {
|
|
566
|
+
const hooks = settings.hooks?.PostToolUse;
|
|
567
|
+
const hasHook = Array.isArray(hooks) && hooks.some((h) =>
|
|
568
|
+
Array.isArray(h.hooks) && h.hooks.some((cmd) =>
|
|
569
|
+
typeof cmd.command === 'string' && cmd.command.includes('sidecar-tool-publisher')));
|
|
570
|
+
if (hasHook) {
|
|
571
|
+
checks.push({ label: 'observability hook', status: 'pass',
|
|
572
|
+
detail: 'sidecar-tool-publisher registered as PostToolUse' });
|
|
573
|
+
} else {
|
|
574
|
+
checks.push({ label: 'observability hook', status: 'warn',
|
|
575
|
+
detail: 'sidecar-tool-publisher not registered',
|
|
576
|
+
fix: 'see kit/hooks/sidecar-tool-publisher.js for installation snippet' });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 5. Bundled kit dirs exist
|
|
581
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
582
|
+
const kitRoot = path.resolve(here, '..', '..', 'kit');
|
|
583
|
+
const expected = ['agents', 'commands', 'skills'];
|
|
584
|
+
const missing = expected.filter((d) => {
|
|
585
|
+
try { return !fs.statSync(path.join(kitRoot, d)).isDirectory(); }
|
|
586
|
+
catch { return true; }
|
|
587
|
+
});
|
|
588
|
+
if (missing.length === 0) {
|
|
589
|
+
checks.push({ label: 'bundled kit', status: 'pass',
|
|
590
|
+
detail: `agents/, commands/, skills/ found in ${kitRoot}` });
|
|
591
|
+
} else {
|
|
592
|
+
checks.push({ label: 'bundled kit', status: 'fail',
|
|
593
|
+
detail: `missing: ${missing.join(', ')} in ${kitRoot}`,
|
|
594
|
+
fix: 'reinstall via `npm i -g @luanpdd/kit-mcp@latest`' });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 6. .planning/ in projectRoot — only warn if absent (not all projects use the framework)
|
|
598
|
+
const planningDir = path.join(projectRoot, '.planning');
|
|
599
|
+
if (fs.existsSync(planningDir)) {
|
|
600
|
+
const stateOk = fs.existsSync(path.join(planningDir, 'STATE.md'));
|
|
601
|
+
const roadmapOk = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
|
602
|
+
if (stateOk && roadmapOk) {
|
|
603
|
+
checks.push({ label: '.planning/', status: 'pass',
|
|
604
|
+
detail: 'STATE.md + ROADMAP.md present' });
|
|
605
|
+
} else {
|
|
606
|
+
checks.push({ label: '.planning/', status: 'warn',
|
|
607
|
+
detail: `present but missing ${[!stateOk && 'STATE.md', !roadmapOk && 'ROADMAP.md'].filter(Boolean).join(', ')}`,
|
|
608
|
+
fix: 'run `kit saude` to repair, or `/novo-marco` if mid-cycle' });
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
checks.push({ label: '.planning/', status: 'warn',
|
|
612
|
+
detail: 'no framework state in this project',
|
|
613
|
+
fix: 'run `/novo-projeto` to bootstrap, or skip if not using the framework' });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 7. Stale lockfile cleanup hint
|
|
617
|
+
try {
|
|
618
|
+
const tmpdir = os.tmpdir();
|
|
619
|
+
const orphans = fs.readdirSync(tmpdir).filter(n => /^kit-mcp-ui-[0-9a-f]{16}\.lock$/.test(n));
|
|
620
|
+
const stale = [];
|
|
621
|
+
for (const name of orphans) {
|
|
622
|
+
try {
|
|
623
|
+
const lock = JSON.parse(fs.readFileSync(path.join(tmpdir, name), 'utf8'));
|
|
624
|
+
try { process.kill(lock.pid, 0); } catch (err) {
|
|
625
|
+
if (err.code === 'ESRCH') stale.push(name);
|
|
626
|
+
}
|
|
627
|
+
} catch { /* skip unreadable */ }
|
|
628
|
+
}
|
|
629
|
+
if (stale.length > 0) {
|
|
630
|
+
checks.push({ label: 'orphan lockfiles', status: 'warn',
|
|
631
|
+
detail: `${stale.length} stale lockfile(s) in ${tmpdir}`,
|
|
632
|
+
fix: stale.map(n => `rm "${path.join(tmpdir, n)}"`).join(' && ') });
|
|
633
|
+
}
|
|
634
|
+
} catch { /* tmpdir scan is best-effort */ }
|
|
635
|
+
|
|
636
|
+
return checks;
|
|
637
|
+
}
|
|
638
|
+
|
|
460
639
|
// Helpers for kit ui (live in cli/ — stdout/console allowed here)
|
|
461
640
|
async function postShutdown(port) {
|
|
462
641
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// upgrade-check.js — non-blocking check for newer kit-mcp on npm.
|
|
2
|
+
//
|
|
3
|
+
// Both `kit doctor` (U1) and `kit ui start` (U4) call this. Result is cached
|
|
4
|
+
// to ~/.kit-mcp/version-check.json for 24h so we don't hit the npm registry on
|
|
5
|
+
// every boot. Falls back gracefully when offline or when the request fails.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import https from 'node:https';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const PACKAGE_NAME = '@luanpdd/kit-mcp';
|
|
17
|
+
const CHECK_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
18
|
+
const REQUEST_TIMEOUT_MS = 1500;
|
|
19
|
+
|
|
20
|
+
function cacheFile() {
|
|
21
|
+
return path.join(os.homedir(), '.kit-mcp', 'version-check.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readCache() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = await fs.readFile(cacheFile(), 'utf8');
|
|
27
|
+
const obj = JSON.parse(raw);
|
|
28
|
+
if (typeof obj.checkedAt !== 'number' || typeof obj.latest !== 'string') return null;
|
|
29
|
+
if (Date.now() - obj.checkedAt > CHECK_TTL_MS) return null;
|
|
30
|
+
return obj;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function writeCache(obj) {
|
|
37
|
+
try {
|
|
38
|
+
await fs.mkdir(path.dirname(cacheFile()), { recursive: true });
|
|
39
|
+
await fs.writeFile(cacheFile(), JSON.stringify(obj), 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
/* cache failures are silent — not critical */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fetchLatest() {
|
|
46
|
+
// Use the registry's package endpoint. Falls back to gracefully on any error.
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const req = https.request({
|
|
49
|
+
method: 'GET',
|
|
50
|
+
hostname: 'registry.npmjs.org',
|
|
51
|
+
path: `/${encodeURIComponent(PACKAGE_NAME)}/latest`,
|
|
52
|
+
headers: { 'accept': 'application/json' },
|
|
53
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
54
|
+
}, (res) => {
|
|
55
|
+
if (res.statusCode !== 200) { res.resume(); resolve(null); return; }
|
|
56
|
+
let body = '';
|
|
57
|
+
res.setEncoding('utf8');
|
|
58
|
+
res.on('data', (c) => { body += c; });
|
|
59
|
+
res.on('end', () => {
|
|
60
|
+
try {
|
|
61
|
+
const j = JSON.parse(body);
|
|
62
|
+
resolve(typeof j.version === 'string' ? j.version : null);
|
|
63
|
+
} catch {
|
|
64
|
+
resolve(null);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
req.on('error', () => resolve(null));
|
|
69
|
+
req.on('timeout', () => { try { req.destroy(); } catch { /* noop */ } resolve(null); });
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getLocalVersion() {
|
|
75
|
+
// Read package.json from the kit-mcp install root (parent of src/).
|
|
76
|
+
try {
|
|
77
|
+
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
78
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
79
|
+
const j = JSON.parse(raw);
|
|
80
|
+
return typeof j.version === 'string' ? j.version : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Compare semver-like x.y.z lexically by component. Returns -1/0/1.
|
|
87
|
+
// Missing components default to 0 ("1.5" === "1.5.0").
|
|
88
|
+
function compareVersions(a, b) {
|
|
89
|
+
const parse = (v) => {
|
|
90
|
+
const parts = v.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
91
|
+
while (parts.length < 3) parts.push(0);
|
|
92
|
+
return parts;
|
|
93
|
+
};
|
|
94
|
+
const [a1, a2, a3] = parse(a);
|
|
95
|
+
const [b1, b2, b3] = parse(b);
|
|
96
|
+
if (a1 !== b1) return a1 < b1 ? -1 : 1;
|
|
97
|
+
if (a2 !== b2) return a2 < b2 ? -1 : 1;
|
|
98
|
+
if (a3 !== b3) return a3 < b3 ? -1 : 1;
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// checkUpgrade({ force }): returns { local, latest, behind, source } or null on failure.
|
|
103
|
+
// force=true bypasses the 24h cache.
|
|
104
|
+
export async function checkUpgrade({ force = false } = {}) {
|
|
105
|
+
const local = await getLocalVersion();
|
|
106
|
+
if (!local) return null;
|
|
107
|
+
|
|
108
|
+
if (!force) {
|
|
109
|
+
const cached = await readCache();
|
|
110
|
+
if (cached?.latest) {
|
|
111
|
+
return {
|
|
112
|
+
local,
|
|
113
|
+
latest: cached.latest,
|
|
114
|
+
behind: compareVersions(local, cached.latest) < 0,
|
|
115
|
+
source: 'cache',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const latest = await fetchLatest();
|
|
121
|
+
if (!latest) {
|
|
122
|
+
// Network failed; surface what we have.
|
|
123
|
+
return { local, latest: null, behind: false, source: 'offline' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await writeCache({ checkedAt: Date.now(), latest });
|
|
127
|
+
return {
|
|
128
|
+
local,
|
|
129
|
+
latest,
|
|
130
|
+
behind: compareVersions(local, latest) < 0,
|
|
131
|
+
source: 'network',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const __test = { compareVersions, PACKAGE_NAME, CHECK_TTL_MS };
|
package/src/core/gates.js
CHANGED
|
@@ -22,7 +22,19 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
22
22
|
const __dirname = path.dirname(__filename);
|
|
23
23
|
export const DEFAULT_GATES_ROOT = path.resolve(__dirname, '../../gates');
|
|
24
24
|
|
|
25
|
+
// P2: TTL cache for listGates (mirrors PERF-01 in kit.js). Gates change rarely;
|
|
26
|
+
// inside a single Claude Code session we may call listGates → getGate → gatesForStage
|
|
27
|
+
// in sequence — without cache, that's 3 full directory walks of the gates dir.
|
|
28
|
+
const GATES_CACHE_TTL_MS = 30_000;
|
|
29
|
+
const gatesCache = new Map(); // gatesRoot -> { value, ts }
|
|
30
|
+
|
|
31
|
+
export function clearGatesCache() { gatesCache.clear(); }
|
|
32
|
+
|
|
25
33
|
export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
|
|
34
|
+
const cached = gatesCache.get(gatesRoot);
|
|
35
|
+
if (cached && Date.now() - cached.ts < GATES_CACHE_TTL_MS) {
|
|
36
|
+
return cached.value;
|
|
37
|
+
}
|
|
26
38
|
let entries;
|
|
27
39
|
try { entries = await fs.readdir(gatesRoot, { withFileTypes: true }); }
|
|
28
40
|
catch { return []; }
|
|
@@ -40,7 +52,9 @@ export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
|
|
|
40
52
|
absPath: abs,
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
|
-
|
|
55
|
+
const value = out.sort((a, b) => a.id.localeCompare(b.id));
|
|
56
|
+
gatesCache.set(gatesRoot, { value, ts: Date.now() });
|
|
57
|
+
return value;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
export async function getGate(id, gatesRoot = DEFAULT_GATES_ROOT) {
|