Merge remote-tracking branch 'upstream/next' into kimi-runtime-support
# Conflicts: # bin/install.js # src/runtime-homes.cts
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user