Merge remote-tracking branch 'upstream/next' into kimi-runtime-support

# Conflicts:
#	bin/install.js
#	src/runtime-homes.cts
This commit is contained in:
Viktorplus
2026-06-07 22:27:05 +02:00
33 changed files with 3273 additions and 124 deletions

View File

@@ -363,6 +363,62 @@ function stageSkillsForRuntimeAsSkills(
return stageDir;
}
/**
* Stage converted command files as flat `.md` files.
*
* Analogous to `stageSkillsForRuntimeAsSkills` but for runtimes that use a
* flat commands directory (e.g. Cursor's `.cursor/commands/<name>.md`).
* Each source `.md` is passed through `converter` and written as a single flat
* `${stem}.md` file in the staging directory (no subdirectory, no prefix).
*
* The `_copyStaged` commands branch in install.js will add the prefix when
* copying staged files to the destination directory, so staged files must be
* named with just the stem (e.g. `help.md` not `gsd-help.md`).
*
* The `converter` receives `(content, ${prefix}${stem})` so it can embed the
* full command name (e.g. 'gsd-help') into the document body if needed.
*
* Used by the `convertedCommandsKind` layout descriptor in
* runtime-artifact-layout.cts (#785 — Cursor 1.6 slash commands).
*
* @param srcCommandsDir source commands directory (e.g. commands/gsd/)
* @param resolvedProfile profile filter — '*' for all, Set for subset
* @param converter (content, commandName) → string pure converter
* @param prefix command name prefix (for converter arg), e.g. 'gsd-'
*/
function stageCommandsForRuntimeFlat(
srcCommandsDir: string,
resolvedProfile: ResolvedProfile,
converter: (content: string, commandName: string) => string,
prefix: string,
): string {
if (!fs.existsSync(srcCommandsDir)) return srcCommandsDir;
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-runtime-commands-'));
try {
const entries = fs.readdirSync(srcCommandsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.md')) continue;
const stem = entry.name.slice(0, -3);
if (resolvedProfile.skills !== '*' && !(resolvedProfile.skills).has(stem)) continue;
const content = fs.readFileSync(path.join(srcCommandsDir, entry.name), 'utf8');
// Pass the full command name (with prefix) to the converter so it can
// reference the installed command name in the body (e.g. for descriptions).
// The staged file itself is named without the prefix; _copyStaged adds it.
const commandName = `${prefix}${stem}`;
const converted = converter(content, commandName);
fs.writeFileSync(path.join(stageDir, `${stem}.md`), converted);
}
} catch (err) {
try { fs.rmSync(stageDir, { recursive: true, force: true }); } catch { /* best-effort */ }
throw err;
}
STAGED_DIRS.add(stageDir);
ensureExitCleanup();
return stageDir;
}
// ---------------------------------------------------------------------------
// Profile marker persistence
// ---------------------------------------------------------------------------
@@ -535,6 +591,7 @@ export = {
stageSkillsForProfile,
stageAgentsForProfile,
stageSkillsForRuntimeAsSkills,
stageCommandsForRuntimeFlat,
STAGED_DIRS,
readActiveProfile,
writeActiveProfile,

View File

@@ -22,6 +22,7 @@ const {
stageSkillsForProfile,
stageAgentsForProfile,
stageSkillsForRuntimeAsSkills,
stageCommandsForRuntimeFlat,
} = installProfiles;
// In .cts (CommonJS output) files, `require` is available as a global.
@@ -271,6 +272,40 @@ function skillsKind(
};
}
/**
* Build a converted-commands kind descriptor for runtimes that use a flat
* commands directory with per-file conversion (e.g. Cursor 1.6 slash commands).
*
* Unlike `commandsKind` (which passes raw source files through), this kind
* applies `converterName` from bin/install.js exports to each file during
* staging, writing flat `${prefix}${stem}.md` files to the staged directory.
*
* The staged files are then written by `_copyStaged` (commands branch) which
* handles prefix logic via the existing layout machinery.
*
* @param destSubpath destination subpath within configDir (e.g. 'commands')
* @param prefix filename prefix, e.g. 'gsd-'
* @param converterName name of converter function in bin/install.js exports
* @param configDir runtime config dir (for .gsd-source marker resolution)
*/
function convertedCommandsKind(
destSubpath: string,
prefix: string,
converterName: string,
configDir: string,
): ArtifactKind {
return {
kind: 'commands',
destSubpath,
prefix,
stage: (resolved) => {
const installExports = getInstallExports();
const converter = installExports[converterName] as (content: string, commandName: string) => string;
return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
},
};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
@@ -303,7 +338,14 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'cursor':
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir)];
// Cursor 1.6+ supports two artifact surfaces:
// 1. skills/gsd-<name>/SKILL.md — rich skills with frontmatter + adapter header
// 2. commands/gsd-<name>.md — plain markdown slash commands (no frontmatter)
// accessed via '/' in the Agent input (#785)
kinds = [
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir),
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir),
];
break;
case 'gemini':
@@ -327,7 +369,10 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'augment':
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir)];
kinds = [
commandsKind('commands', 'gsd-', configDir),
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir),
];
break;
case 'trae':
@@ -347,7 +392,7 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'cline':
kinds = [];
kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir)] : [];
break;
case 'kimi':
@@ -360,11 +405,21 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'opencode':
kinds = [commandsKind('command', 'gsd-', configDir)];
// OpenCode reads flat slash commands from command/ and on-demand skills
// from skills/<name>/SKILL.md (https://opencode.ai/docs/skills). Emit both.
kinds = [
commandsKind('command', 'gsd-', configDir),
skillsKind('skills', 'gsd-', 'convertClaudeCommandToOpencodeSkill', 'opencode', configDir),
];
break;
case 'kilo':
kinds = [commandsKind('command', 'gsd-', configDir)];
// Kilo derives from OpenCode and shares the skills/<name>/SKILL.md layout
// (https://kilo.ai/docs/customize/skills). Emit flat commands + skills.
kinds = [
commandsKind('command', 'gsd-', configDir),
skillsKind('skills', 'gsd-', 'convertClaudeCommandToKiloSkill', 'kilo', configDir),
];
break;
default:

View File

@@ -11,9 +11,9 @@
* Runtime-specific notes:
* hermes — GSD skills nest under skills/gsd/<skillName>/ (not the flat
* skills/<skillName>/ layout used by all other runtimes).
* cline — Rules-based; commands are embedded in .clinerules. Cline does
* not use a skills/ directory. getGlobalSkillDir() returns null
* for cline so the caller can emit an appropriate warning.
* cline — Skills-capable since v3.48.0 (#782). SKILL.md files live at
* ~/.cline/skills/<skillName>/SKILL.md (same flat layout as cursor/codex).
* .clinerules is also emitted (rules-based compatibility layer).
* kimi — Agent Skills are discovered from Kimi's generic user roots:
* ~/.config/agents/skills (recommended) then ~/.agents/skills,
* with Kimi selecting the first existing generic skills directory.
@@ -135,7 +135,9 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null)
// ── Copilot (VS Code) ────────────────────────────────────────────────────
case 'copilot':
return env['COPILOT_CONFIG_DIR'] ? expandTilde(env['COPILOT_CONFIG_DIR']) : path.join(home, '.copilot');
if (env['COPILOT_CONFIG_DIR']) return expandTilde(env['COPILOT_CONFIG_DIR']);
if (env['COPILOT_HOME']) return expandTilde(env['COPILOT_HOME']);
return path.join(home, '.copilot');
// ── Antigravity ──────────────────────────────────────────────────────────
case 'antigravity':
@@ -202,18 +204,25 @@ 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: null (rules-based, no skills directory)
* Cline ≥ v3.48.0: <configDir>/skills (SKILL.md-based global skills — #782)
*/
export function getGlobalSkillsBase(runtime: string): string | null {
if (runtime === 'cline') return null;
if (runtime === 'hermes') {
const configDir = getGlobalConfigDir(runtime);
return path.join(configDir, 'skills', 'gsd');
}
// 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);
if (runtime === 'hermes') return path.join(configDir, 'skills', 'gsd');
return path.join(configDir, 'skills');
}
/**
* Return the full path to a specific skill's directory for the given runtime.
* Returns null for runtimes that don't use a skills directory (cline).
*/
export function getGlobalSkillDir(runtime: string, skillName: string): string | null {
const base = getGlobalSkillsBase(runtime);