/** * Agent Install Check — moved from core.cts (ADR-857 T0 #1268 phase rehome-core-squatters). * * Owns: * - getAgentsDir(runtime?): string * - checkAgentsInstalled(runtime?): AgentsInstalledResult * * The core.cjs re-export spine was retired in epic #1267; callers import * these symbols from agent-install-check.cjs directly. */ import fs from 'node:fs'; import path from 'node:path'; // eslint-disable-next-line @typescript-eslint/no-require-imports import modelProfiles = require('./model-profiles.cjs'); const { MODEL_PROFILES } = modelProfiles; import { getGlobalConfigDir } from './runtime-homes.cjs'; interface AgentsInstalledResult { agents_installed: boolean; missing_agents: string[]; installed_agents: string[]; incomplete_agents: string[]; agents_dir: string; agent_runtime: string; } /** * Resolve the agents directory for the given runtime. * * Priority: * 1. GSD_AGENTS_DIR env var (explicit override, any runtime) * 2. For claude runtime: __dirname-relative path (agents/ sibling of gsd-core/) * This is correct for both repo runs and real installs (the runtime config dir's * agents/ folder) because gsd-tools.cjs lives inside gsd-core/bin/ in both cases. * 3. For non-claude runtimes: getGlobalConfigDir(runtime)/agents * * @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude' */ function getAgentsDir(runtime?: string): string { if (process.env['GSD_AGENTS_DIR']) { return process.env['GSD_AGENTS_DIR']; } const resolved = runtime ?? (process.env['GSD_RUNTIME'] || 'claude'); if (resolved === 'claude') { return path.join(__dirname, '..', '..', '..', 'agents'); } return path.join(getGlobalConfigDir(resolved), 'agents'); } /** * Check which GSD agents are installed on disk. * * @param runtime - the active runtime name; defaults to GSD_RUNTIME env, then 'claude' */ function checkAgentsInstalled(runtime?: string): AgentsInstalledResult { const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude'); const agentsDir = getAgentsDir(resolvedRuntime); const expectedAgents = Object.keys(MODEL_PROFILES); const installed: string[] = []; const missing: string[] = []; if (!fs.existsSync(agentsDir)) { return { agents_installed: false, missing_agents: expectedAgents, installed_agents: [], incomplete_agents: [], agents_dir: agentsDir, agent_runtime: resolvedRuntime, }; } for (const agent of expectedAgents) { const agentFile = path.join(agentsDir, `${agent}.md`); const agentFileCopilot = path.join(agentsDir, `${agent}.agent.md`); const agentFileCodex = path.join(agentsDir, `${agent}.toml`); const agentFileKimiYaml = path.join(agentsDir, 'subagents', `${agent}.yaml`); const agentFileKimiPrompt = path.join(agentsDir, 'subagents', `${agent}.md`); const kimiAgentInstalled = resolvedRuntime === 'kimi' && fs.existsSync(agentFileKimiYaml) && fs.existsSync(agentFileKimiPrompt); if ( fs.existsSync(agentFile) || fs.existsSync(agentFileCopilot) || fs.existsSync(agentFileCodex) || kimiAgentInstalled ) { installed.push(agent); } else { missing.push(agent); } } // ── Manifest-backed completeness check ────────────────────────────────────── // If a gsd-file-manifest.json exists alongside the agents dir (parent dir), // verify that every manifest-tracked file for each expected agent is present // on disk. Missing manifest-tracked files indicate an incomplete install even // when the plain presence check above passed (e.g. .md present, .toml absent). // If no manifest is found the check is a no-op (graceful for claude/bundled). const incomplete: string[] = []; const manifestPath = path.join(path.dirname(agentsDir), 'gsd-file-manifest.json'); let manifestFiles: Record = {}; try { const raw = fs.readFileSync(manifestPath, 'utf8'); const parsed: unknown = JSON.parse(raw); if ( parsed !== null && typeof parsed === 'object' && 'files' in parsed && typeof (parsed as Record)['files'] === 'object' && (parsed as Record)['files'] !== null ) { manifestFiles = (parsed as Record>)['files']; } } catch { // No manifest or unreadable — completeness check is skipped } if (Object.keys(manifestFiles).length > 0) { for (const agent of expectedAgents) { // Find all manifest keys that belong to this agent: // key must be "agents/." with no further path segments. const agentPrefix = `agents/${agent}.`; const agentManifestKeys = Object.keys(manifestFiles).filter(key => { if (!key.startsWith(agentPrefix)) return false; const rest = key.slice(agentPrefix.length); // rest must be a bare extension (no slashes, non-empty) return rest.length > 0 && !rest.includes('/'); }); if (agentManifestKeys.length === 0) { // Agent not tracked in manifest — skip completeness check for this agent continue; } const allPresent = agentManifestKeys.every(key => { const basename = key.slice('agents/'.length); return fs.existsSync(path.join(agentsDir, basename)); }); if (!allPresent) { incomplete.push(agent); } } } return { agents_installed: installed.length > 0 && missing.length === 0 && incomplete.length === 0, missing_agents: missing, installed_agents: installed, incomplete_agents: incomplete, agents_dir: agentsDir, agent_runtime: resolvedRuntime, }; } export = { getAgentsDir, checkAgentsInstalled, };