feat(install): validate agent CLI runtime registries
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { onPath } from './lib.js';
|
||||
|
||||
/** Runtime probes are deliberately local, non-interactive, and network-free. */
|
||||
const SPECS = {
|
||||
'oh-my-pi': { binary: 'omp', supported: true, root: (home) => join(home, '.omp', 'agent', 'commands') },
|
||||
claude: { binary: 'claude', supported: true, root: (home) => join(home, '.claude', 'commands') },
|
||||
codex: { binary: 'codex', supported: true, root: (home) => join(home, '.codex', 'prompts') },
|
||||
opencode: { binary: 'opencode', supported: true, root: (home) => join(home, '.config', 'opencode', 'command') },
|
||||
antigravity: { binary: 'gemini', supported: false },
|
||||
kiro: { binary: 'kiro', supported: false },
|
||||
copilot: { binary: 'gh', supported: false },
|
||||
};
|
||||
|
||||
const PROBE_VERSION = 1;
|
||||
|
||||
/**
|
||||
* @param {{name: string, adapters: {name: string, path: string}[]}[]} platforms
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function verifyRuntimePlatforms(platforms) {
|
||||
return platforms.map((platform) => verifyRuntimePlatform(platform));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{name: string, adapters: {name: string, path: string}[]}} platform
|
||||
* @returns {object}
|
||||
*/
|
||||
function verifyRuntimePlatform(platform) {
|
||||
const spec = SPECS[platform.name];
|
||||
if (!spec || !spec.supported) {
|
||||
return {
|
||||
name: platform.name,
|
||||
status: 'not-supported',
|
||||
probe: { command: spec?.binary ?? null, args: [], network: false, interactive: false, version: PROBE_VERSION },
|
||||
resolved: spec ? onPath(spec.binary) : null,
|
||||
commands: [],
|
||||
missing: [],
|
||||
failures: [],
|
||||
remediation: '此 AI Agent CLI 沒有可供 tea-sdlc 安全呼叫的 command registry probe;請在該 CLI 內手動確認指令。',
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = onPath(spec.binary);
|
||||
const probe = {
|
||||
command: 'filesystem-registry',
|
||||
args: [spec.binary],
|
||||
network: false,
|
||||
interactive: false,
|
||||
version: PROBE_VERSION,
|
||||
};
|
||||
if (resolved === null) {
|
||||
return unsupportedRuntime(
|
||||
platform,
|
||||
probe,
|
||||
null,
|
||||
`找不到 ${spec.binary},沒有可安全執行的 runtime probe`,
|
||||
`請安裝 ${spec.binary} 並把它加入 PATH 後再重跑 tea-sdlc verify`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const root = dirname(platform.adapters[0]?.path ?? spec.root(process.env.HOME || homedir()));
|
||||
const expected = platform.adapters.map(({ name }) => name);
|
||||
const available = new Set(
|
||||
existsSync(root)
|
||||
? readdirSync(root, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map((entry) => basename(entry.name, '.md'))
|
||||
: [],
|
||||
);
|
||||
const commands = expected.filter((name) => available.has(name));
|
||||
const missing = expected.filter((name) => !available.has(name));
|
||||
if (missing.length > 0) {
|
||||
return failedRuntime(
|
||||
platform,
|
||||
{ ...probe, registry: root },
|
||||
resolved,
|
||||
`runtime registry 找不到:${missing.join('、')}`,
|
||||
`確認 ${root} 是 ${spec.binary} 的 command registry,然後重跑 tea-sdlc install`,
|
||||
commands,
|
||||
missing,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: platform.name,
|
||||
status: 'pass',
|
||||
probe: { ...probe, registry: root },
|
||||
resolved,
|
||||
commands,
|
||||
missing,
|
||||
failures: [],
|
||||
remediation: null,
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedRuntime(platform, probe, resolved, message, remediation) {
|
||||
return {
|
||||
name: platform.name,
|
||||
status: 'not-supported',
|
||||
probe,
|
||||
resolved,
|
||||
commands: [],
|
||||
missing: platform.adapters.map(({ name }) => name),
|
||||
failures: [],
|
||||
remediation: `${message};${remediation}`,
|
||||
};
|
||||
}
|
||||
|
||||
function failedRuntime(platform, probe, resolved, message, remediation, commands = [], missing = platform.adapters.map(({ name }) => name)) {
|
||||
const failure = {
|
||||
path: probe.registry ?? null,
|
||||
病灶: message,
|
||||
修復: remediation,
|
||||
};
|
||||
return {
|
||||
name: platform.name,
|
||||
status: 'fail',
|
||||
probe,
|
||||
resolved,
|
||||
commands,
|
||||
missing,
|
||||
failures: [failure],
|
||||
remediation,
|
||||
};
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
export function runtimeProbeVersion() {
|
||||
return PROBE_VERSION;
|
||||
}
|
||||
Reference in New Issue
Block a user