* feat(#785): write .cursor/commands/ as Cursor 1.6 slash-command surface Cursor 1.6 (released 2025-09-12) introduced plain-markdown slash commands in `.cursor/commands/<name>.md` — no frontmatter, invocable via `/` in the Agent input. GSD previously emitted only `~/.cursor/skills/` for Cursor. This PR wires a second artifact kind for `cursor` in `runtime-artifact-layout.cts`: `convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir)`. The new kind applies the same `convertClaudeToCursorMarkdown` transforms (tool renames, brand substitution, slash-command normalisation) and then strips YAML frontmatter so the output is plain prose. Skills output is unchanged. `stageCommandsForRuntimeFlat` in `install-profiles.cts` stages each source `.md` as a flat `<stem>.md` in a temp dir; the existing `_copyStaged` commands path then prefixes and copies to `<configDir>/commands/`. `.cursor/mcp.json` is explicitly OUT OF SCOPE: GSD ships no MCP server; the `mcpServers` schema cannot be usefully populated by the installer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(#785): address review nit --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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,
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
stageSkillsForProfile,
|
||||
stageAgentsForProfile,
|
||||
stageSkillsForRuntimeAsSkills,
|
||||
stageCommandsForRuntimeFlat,
|
||||
} = installProfiles;
|
||||
|
||||
// In .cts (CommonJS output) files, `require` is available as a global.
|
||||
@@ -225,6 +226,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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -257,7 +292,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':
|
||||
|
||||
Reference in New Issue
Block a user