@@ -112,7 +112,9 @@ function parseCallsAgents(content: string): string[] {
|
||||
* `gsd-*` agent name references. Agent stems are stored under the special
|
||||
* key `_calls_agents_<stem>` so they don't conflict with skill stems.
|
||||
*/
|
||||
function loadSkillsManifest(commandsDir: string): Map<string, string[]> {
|
||||
const DEFAULT_COMMANDS_DIR = path.resolve(__dirname, '..', '..', '..', 'commands', 'gsd');
|
||||
|
||||
function loadSkillsManifest(commandsDir: string = DEFAULT_COMMANDS_DIR): Map<string, string[]> {
|
||||
const manifest = new Map<string, string[]>();
|
||||
if (!fs.existsSync(commandsDir)) return manifest;
|
||||
const entries = fs.readdirSync(commandsDir, { withFileTypes: true });
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* Runtime artifact layout module — resolves the artifact directory shapes
|
||||
* (commands, agents, skills) for each supported runtime.
|
||||
*
|
||||
* grok is intentionally absent: it is in runtime-homes.cjs but not wired
|
||||
* here. The TypeError on unknown runtime is the loud-fail signal that a
|
||||
* runtime was added to the homes list without a layout entry.
|
||||
* grok is intentionally absent: it is in runtime-homes.cjs but has no runtime
|
||||
* capability descriptor. The TypeError on unknown runtime is the loud-fail
|
||||
* signal that a runtime was added without an artifact layout descriptor.
|
||||
*
|
||||
* ADR-457 build-at-publish: the hand-written bin/lib/runtime-artifact-layout.cjs
|
||||
* collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
||||
@@ -166,16 +166,6 @@ function findAgentsSourceRoot(runtimeConfigDir?: string): string {
|
||||
throw new Error(`findAgentsSourceRoot: could not locate agents/ from ${__dirname}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Allowlisted runtimes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ALLOWED_RUNTIMES = new Set([
|
||||
'claude', 'cursor', 'gemini', 'codex', 'copilot', 'antigravity',
|
||||
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy',
|
||||
'cline', 'kimi', 'opencode', 'kilo',
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout table builders
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -382,7 +372,11 @@ interface ArtifactLayoutDescriptor {
|
||||
}
|
||||
|
||||
/** Lazy registry accessor — mirrors pattern from 5b/5c (runtime-homes.cts). */
|
||||
function getRegistry(): { runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }> } {
|
||||
interface RegistryLike {
|
||||
runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }>;
|
||||
}
|
||||
|
||||
function getRegistry(): RegistryLike {
|
||||
return _require('./capability-registry.cjs') as {
|
||||
runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }>;
|
||||
};
|
||||
@@ -431,19 +425,24 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
|
||||
* instead of a hardcoded switch statement.
|
||||
*/
|
||||
function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: 'local' | 'global' = 'global'): Layout {
|
||||
return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope);
|
||||
}
|
||||
|
||||
function resolveRuntimeArtifactLayoutFromRegistry(
|
||||
registry: RegistryLike,
|
||||
runtime: string,
|
||||
configDir: string,
|
||||
scope: 'local' | 'global' = 'global',
|
||||
): Layout {
|
||||
if (typeof configDir !== 'string' || configDir === '') {
|
||||
throw new TypeError('configDir must be a non-empty string');
|
||||
}
|
||||
if (scope !== 'local' && scope !== 'global') {
|
||||
throw new TypeError('scope must be "local" or "global"');
|
||||
}
|
||||
if (!ALLOWED_RUNTIMES.has(runtime)) {
|
||||
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
||||
}
|
||||
|
||||
const desc = getRegistry().runtimes[runtime]?.runtime?.artifactLayout;
|
||||
const desc = registry.runtimes[runtime]?.runtime?.artifactLayout;
|
||||
if (!desc) {
|
||||
// Runtime is in ALLOWED_RUNTIMES but has no descriptor — reproduce old default: throw.
|
||||
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
||||
}
|
||||
|
||||
@@ -453,4 +452,4 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
|
||||
return { runtime, configDir, scope, kinds };
|
||||
}
|
||||
|
||||
export = { resolveRuntimeArtifactLayout, findInstallSourceRoot, getInstallExports };
|
||||
export = { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, findInstallSourceRoot, getInstallExports };
|
||||
|
||||
@@ -82,6 +82,8 @@ interface InstallPlan extends RuntimeConfigIntent {
|
||||
// Exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type RuntimeDescriptorMap = Record<string, { runtime: Record<string, unknown> | undefined }>;
|
||||
|
||||
/** The complete set of 16 supported runtimes for config-adapter dispatch. */
|
||||
const ALLOWED_CONFIG_RUNTIMES: ReadonlySet<string> = new Set(
|
||||
Object.entries(runtimes)
|
||||
@@ -119,6 +121,29 @@ function resolveRuntimeConfigIntent(runtime: string): RuntimeConfigIntent {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveInstallPlanFromRuntimes(runtimeDescriptors: RuntimeDescriptorMap, runtime: string): InstallPlan {
|
||||
const desc = runtimeDescriptors[runtime]?.runtime;
|
||||
if (!desc) throw new TypeError(`Unknown runtime for install plan: ${runtime}`);
|
||||
if (desc['hooksSurface'] == null) {
|
||||
throw new TypeError(`runtime.hooksSurface is required for install plan: ${runtime}`);
|
||||
}
|
||||
const sandboxTier = desc['sandboxTier'];
|
||||
if (typeof sandboxTier !== 'string' || !VALID_SANDBOX_TIERS.has(sandboxTier)) {
|
||||
throw new TypeError(`Runtime '${runtime}' has a missing or invalid sandboxTier descriptor axis: ${JSON.stringify(sandboxTier)}`);
|
||||
}
|
||||
const permissionWriter = desc['permissionWriter'];
|
||||
return {
|
||||
runtime,
|
||||
installSurface: desc['installSurface'] as ConfigInstallSurface,
|
||||
writesSharedSettings: desc['writesSharedSettings'] as boolean,
|
||||
finishPermissionWriter: permissionWriter == null ? null : permissionWriter as FinishPermissionWriter,
|
||||
hookEvents: desc['hookEvents'] as string | undefined,
|
||||
extendedHookEvents: Array.isArray(desc['extendedHookEvents']) ? [...desc['extendedHookEvents'] as string[]] : [],
|
||||
hooksSurface: desc['hooksSurface'] as HooksSurface,
|
||||
sandboxTier,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the complete install plan for a given runtime.
|
||||
*
|
||||
@@ -132,25 +157,7 @@ function resolveRuntimeConfigIntent(runtime: string): RuntimeConfigIntent {
|
||||
* @throws {TypeError} if runtime is not a known supported runtime.
|
||||
*/
|
||||
function resolveInstallPlan(runtime: string): InstallPlan {
|
||||
const desc = runtimes[runtime]?.runtime;
|
||||
if (!desc) throw new TypeError(`Unknown runtime for install plan: ${runtime}`);
|
||||
const configIntent = resolveRuntimeConfigIntent(runtime);
|
||||
const sandboxTier = desc['sandboxTier'];
|
||||
if (typeof sandboxTier !== 'string' || !VALID_SANDBOX_TIERS.has(sandboxTier)) {
|
||||
throw new TypeError(`Runtime '${runtime}' has a missing or invalid sandboxTier descriptor axis: ${JSON.stringify(sandboxTier)}`);
|
||||
}
|
||||
return {
|
||||
runtime,
|
||||
installSurface: configIntent.installSurface,
|
||||
writesSharedSettings: configIntent.writesSharedSettings,
|
||||
finishPermissionWriter: configIntent.finishPermissionWriter,
|
||||
hookEvents: desc['hookEvents'] as string | undefined,
|
||||
extendedHookEvents: Array.isArray(desc['extendedHookEvents']) ? desc['extendedHookEvents'] as string[] : [],
|
||||
hooksSurface: desc['hooksSurface'] != null
|
||||
? desc['hooksSurface'] as HooksSurface
|
||||
: ((runtime === 'opencode' || runtime === 'kilo') ? 'none' : 'settings-json'),
|
||||
sandboxTier,
|
||||
};
|
||||
return resolveInstallPlanFromRuntimes(runtimes, runtime);
|
||||
}
|
||||
|
||||
export = { resolveRuntimeConfigIntent, resolveInstallPlan, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };
|
||||
export = { resolveRuntimeConfigIntent, resolveInstallPlan, resolveInstallPlanFromRuntimes, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };
|
||||
|
||||
@@ -67,6 +67,7 @@ interface DotHomeDescriptor {
|
||||
kind: 'dot-home';
|
||||
name: string;
|
||||
env: string[];
|
||||
skillsHome?: ConfigHomeDescriptor;
|
||||
}
|
||||
|
||||
interface DotHomeNestedDescriptor {
|
||||
@@ -75,13 +76,14 @@ interface DotHomeNestedDescriptor {
|
||||
parent: string;
|
||||
env: string[];
|
||||
probe?: string[];
|
||||
skillsHome?: ConfigHomeDescriptor;
|
||||
}
|
||||
|
||||
interface XdgDescriptor {
|
||||
kind: 'xdg';
|
||||
name: string;
|
||||
env: string[];
|
||||
skillsHome?: unknown;
|
||||
skillsHome?: ConfigHomeDescriptor;
|
||||
}
|
||||
|
||||
interface GenericAgentsRootDescriptor {
|
||||
@@ -90,6 +92,7 @@ interface GenericAgentsRootDescriptor {
|
||||
env: string[];
|
||||
probe: string[];
|
||||
probeExists: string;
|
||||
skillsHome?: ConfigHomeDescriptor;
|
||||
}
|
||||
|
||||
type ConfigHomeDescriptor =
|
||||
@@ -98,6 +101,33 @@ type ConfigHomeDescriptor =
|
||||
| XdgDescriptor
|
||||
| GenericAgentsRootDescriptor;
|
||||
|
||||
interface RuntimeArtifactKindDescriptor {
|
||||
kind: string;
|
||||
destSubpath: string;
|
||||
}
|
||||
|
||||
interface RuntimeDescriptor {
|
||||
configHome: ConfigHomeDescriptor;
|
||||
artifactLayout?: {
|
||||
global?: RuntimeArtifactKindDescriptor[];
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDescriptorWithOptions(configHome: ConfigHomeDescriptor): string {
|
||||
return resolveConfigHomeFromDescriptor(configHome, {
|
||||
env: process.env,
|
||||
home: os.homedir(),
|
||||
existsSync: fs.existsSync,
|
||||
});
|
||||
}
|
||||
|
||||
function getRegistry(): { runtimes: Record<string, { runtime?: RuntimeDescriptor }> } {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require('./capability-registry.cjs') as {
|
||||
runtimes: Record<string, { runtime?: RuntimeDescriptor }>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a configHome descriptor to an absolute directory path.
|
||||
*
|
||||
@@ -267,18 +297,11 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null)
|
||||
}
|
||||
|
||||
// ── Descriptor-driven: look up in capability-registry ────────────────────
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { runtimes } = require('./capability-registry.cjs') as {
|
||||
runtimes: Record<string, { runtime?: { configHome: ConfigHomeDescriptor } }>;
|
||||
};
|
||||
const { runtimes } = getRegistry();
|
||||
|
||||
const runtimeEntry = runtimes[runtime];
|
||||
if (runtimeEntry?.runtime?.configHome) {
|
||||
return resolveConfigHomeFromDescriptor(runtimeEntry.runtime.configHome, {
|
||||
env: process.env,
|
||||
home: os.homedir(),
|
||||
existsSync: fs.existsSync,
|
||||
});
|
||||
return resolveDescriptorWithOptions(runtimeEntry.runtime.configHome);
|
||||
}
|
||||
|
||||
// ── Default (unknown runtime → Claude fallback) ───────────────────────────
|
||||
@@ -288,21 +311,30 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null)
|
||||
|
||||
/**
|
||||
* Return the global skills base directory for the given runtime.
|
||||
* Most runtimes: <configDir>/skills
|
||||
* Hermes: <configDir>/skills/gsd (nested category layout — #2841)
|
||||
* Cline ≥ v3.48.0: <configDir>/skills (SKILL.md-based global skills — #782)
|
||||
* Descriptor-backed runtimes derive the base home from configHome.skillsHome
|
||||
* when present, then append the first global skills artifact destSubpath.
|
||||
*/
|
||||
export function resolveSkillsBaseFromDescriptor(
|
||||
configHome: ConfigHomeDescriptor,
|
||||
opts: ResolveConfigHomeOpts = {},
|
||||
skillsDestSubpath = 'skills',
|
||||
): string {
|
||||
const baseDescriptor = configHome.skillsHome ?? configHome;
|
||||
const base = resolveConfigHomeFromDescriptor(baseDescriptor, opts);
|
||||
return path.join(base, skillsDestSubpath);
|
||||
}
|
||||
|
||||
export function getGlobalSkillsBase(runtime: string): string | null {
|
||||
if (runtime === 'hermes') {
|
||||
const configDir = getGlobalConfigDir(runtime);
|
||||
return path.join(configDir, 'skills', 'gsd');
|
||||
const runtimeEntry = getRegistry().runtimes[runtime];
|
||||
const descriptor = runtimeEntry?.runtime;
|
||||
const globalSkillsKind = descriptor?.artifactLayout?.global?.find((entry) => entry.kind === 'skills');
|
||||
if (descriptor?.configHome && globalSkillsKind?.destSubpath) {
|
||||
return resolveSkillsBaseFromDescriptor(
|
||||
descriptor.configHome,
|
||||
{ env: process.env, home: os.homedir(), existsSync: fs.existsSync },
|
||||
globalSkillsKind.destSubpath,
|
||||
);
|
||||
}
|
||||
// Kilo Code discovers global skills from ~/.kilo/skills/ (HOME-relative),
|
||||
// independent of the XDG-based config dir (~/.config/kilo) used for commands.
|
||||
// See: https://kilo.ai/docs/customize/skills
|
||||
// "Global skills are located in the `.kilo` directory within your Home
|
||||
// directory: ~/.kilo/skills/"
|
||||
if (runtime === 'kilo') return path.join(os.homedir(), '.kilo', 'skills');
|
||||
const configDir = getGlobalConfigDir(runtime);
|
||||
return path.join(configDir, 'skills');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user