* refactor(#1294): T-final — delete the core.cjs re-export spine (epic #1267 complete) After T0–T6 nothing imports core, so retire the spine and its scaffolding: - delete src/core.cts (and the gitignored gsd-core/bin/lib/core.cjs artifact; remove its .gitignore + eslint-ignore entries) - delete scripts/lint-core-spine-imports.cjs + its allowlist; drop it from the package.json lint:ci chain - regenerate docs/INVENTORY-MANIFEST.json (drops the core.cjs surface) - sweep stale references: CONTEXT.md glossary back-compat clauses (spine retired, callers import the leaf directly), planning-config.md CONFIG_DEFAULTS owner, and false present-tense core.cjs claims in leaf-module docstrings The ADR-857 decomposition is complete: the former Core god-module is fully dissolved into its leaf modules; no re-export spine remains. No behaviour change. Closes #1294 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1294): migrate the computed-path core.cjs importers the literal grep missed bin/install.js used require(path.join(_gsdLibDir, 'core.cjs')) (a computed path, and bin/install.js was never in the convergence lint's scan roots), and ~8 test files referenced core.cjs via path.join/readFileSync/existsSync/FILE_ARG forms the literal-string migration grep missed. Route install.js's symbols to their leaves (RUNTIME_PROFILE_MAP->model-catalog, resolveTierEntry/EFFORT_SET-> model-resolver) and repoint/adjust the test references to the leaves. Recovers the 161 'Cannot find module core.cjs' failures from the spine deletion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
160 lines
5.7 KiB
TypeScript
160 lines
5.7 KiB
TypeScript
/**
|
|
* 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<string, unknown> = {};
|
|
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<string, unknown>)['files'] === 'object' &&
|
|
(parsed as Record<string, unknown>)['files'] !== null
|
|
) {
|
|
manifestFiles = (parsed as Record<string, Record<string, unknown>>)['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/<agentName>.<ext>" 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,
|
|
};
|