* 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:
5
.changeset/785-cursor-slash-commands.md
Normal file
5
.changeset/785-cursor-slash-commands.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 803
|
||||
---
|
||||
`gsd install --cursor` now writes `.cursor/commands/gsd-<name>.md` in addition to the existing `.cursor/skills/` surface. Cursor 1.6 introduced plain-markdown slash commands (no frontmatter) in `.cursor/commands/`; they appear in the `/` menu in the Agent input. Each command file is generated from the same source as the skill but with frontmatter stripped and Cursor-specific content transforms applied (`convertClaudeCommandToCursorCommand`). The skills surface is unchanged — both surfaces are written on every install.
|
||||
@@ -2196,6 +2196,29 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Claude Code command to a Cursor 1.6 slash command (#785).
|
||||
*
|
||||
* Cursor slash commands live in `.cursor/commands/<name>.md` and are
|
||||
* plain markdown — no YAML frontmatter, no adapter header. The filename
|
||||
* becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
|
||||
*
|
||||
* Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
|
||||
* converter (tool renames, brand substitution, slash-command normalisation),
|
||||
* then strips the YAML frontmatter block so only the prose body remains.
|
||||
*
|
||||
* @param {string} content raw Claude Code command markdown (may have frontmatter)
|
||||
* @param {string} _commandName the target command name (unused; present for
|
||||
* API symmetry with other converters so the runtime-artifact-layout stage
|
||||
* function can call it uniformly)
|
||||
* @returns {string} plain markdown body, no frontmatter
|
||||
*/
|
||||
function convertClaudeCommandToCursorCommand(content, _commandName) {
|
||||
const converted = convertClaudeToCursorMarkdown(content);
|
||||
const { body } = extractFrontmatterAndBody(converted);
|
||||
return body.trimStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Claude Code agent markdown to Cursor agent format.
|
||||
* Strips frontmatter fields Cursor doesn't support (color, skills),
|
||||
@@ -8603,6 +8626,22 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
|
||||
// Cursor only: also report the commands/ output (#785 — Cursor 1.6 slash commands)
|
||||
if (isCursor) {
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
if (fs.existsSync(commandsDir)) {
|
||||
const cmdCount = fs.readdirSync(commandsDir)
|
||||
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
|
||||
if (cmdCount > 0) {
|
||||
console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`);
|
||||
} else {
|
||||
failures.push('commands/gsd-*');
|
||||
}
|
||||
} else {
|
||||
failures.push('commands/gsd-*');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isOpencode || isKilo) {
|
||||
// OpenCode/Kilo: flat structure in command/ directory
|
||||
@@ -10895,6 +10934,7 @@ module.exports = {
|
||||
computePathPrefix,
|
||||
getCodexSkillAdapterHeader,
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeCommandToCursorCommand,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
convertClaudeToGeminiMarkdown,
|
||||
convertSlashCommandsToGeminiMentions,
|
||||
|
||||
@@ -1013,12 +1013,16 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
**Runtime Transformations:**
|
||||
|
||||
| Aspect | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity | Trae | Cline | Augment | CodeBuddy | Qwen Code |
|
||||
|--------|------------|----------|--------|-------|-------|---------|-------------|------|-------|---------|-----------|-----------|
|
||||
| Commands | Slash commands | Slash commands | Slash commands | Slash commands | Skills (TOML) | Slash commands | Skills | Skills | Rules | Skills | Skills | Skills |
|
||||
| Agent format | Claude native | `mode: subagent` | Claude native | `mode: subagent` | Skills | Tool mapping | Skills | Skills | Rules | Skills | Skills | Skills |
|
||||
| Hook events | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| Config | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config | Config | `.clinerules` | Config | Config | Config |
|
||||
| Aspect | Claude Code | OpenCode | Gemini | Kilo | Codex | Copilot | Antigravity | Cursor | Trae | Cline | Augment | CodeBuddy | Qwen Code |
|
||||
|--------|------------|----------|--------|-------|-------|---------|-------------|--------|------|-------|---------|-----------|-----------|
|
||||
| Commands | Slash commands | Slash commands | Slash commands | Slash commands | Skills (TOML) | Slash commands | Skills | Skills + Slash commands | Skills | Rules | Skills | Skills | Skills |
|
||||
| Agent format | Claude native | `mode: subagent` | Claude native | `mode: subagent` | Skills | Tool mapping | Skills | Skills | Skills | Rules | Skills | Skills | Skills |
|
||||
| Hook events | `PostToolUse` | N/A | `AfterTool` | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| Config | `settings.json` | `opencode.json(c)` | `settings.json` | `kilo.json(c)` | TOML | Instructions | Config | Config | Config | `.clinerules` | Config | Config | Config |
|
||||
|
||||
**Cursor artifact surfaces:** `gsd install --cursor` writes two artifact kinds:
|
||||
- `~/.cursor/skills/gsd-<name>/SKILL.md` — rich skills with YAML frontmatter, Cursor tool-name mapping, and adapter context header (existing surface)
|
||||
- `~/.cursor/commands/gsd-<name>.md` — plain markdown slash commands (no frontmatter) invocable via `/` in the Agent input (Cursor 1.6+, added in #785)
|
||||
|
||||
**Claude Code native plugin distribution:** GSD Core ships a `.claude-plugin/plugin.json` manifest, enabling installation and lifecycle management via `claude plugin install|enable|disable|update gsd-core`. Commands load under the `/gsd-core:` namespace (e.g. `/gsd-core:plan-phase`), avoiding slash-command collisions with the classic npm installer which uses `/gsd:`. Always-on guard and update hooks are wired automatically via `hooks/hooks.json`. The plugin path is additive — the npm installer (`npx @opengsd/gsd-core`) remains fully supported.
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* Ensures Cursor frontmatter names are emitted as plain identifiers
|
||||
* (without surrounding quotes), so Cursor does not treat quotes as
|
||||
* literal parts of skill/subagent names.
|
||||
*
|
||||
* Also covers convertClaudeCommandToCursorCommand (#785 — Cursor 1.6
|
||||
* slash commands via .cursor/commands/).
|
||||
*/
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
@@ -14,6 +17,7 @@ const assert = require('node:assert/strict');
|
||||
const {
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
convertClaudeCommandToCursorCommand,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
describe('convertClaudeCommandToCursorSkill', () => {
|
||||
@@ -79,3 +83,71 @@ Planner body
|
||||
assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── convertClaudeCommandToCursorCommand (#785) ───────────────────────────────
|
||||
|
||||
describe('convertClaudeCommandToCursorCommand (#785 — Cursor 1.6 .cursor/commands/)', () => {
|
||||
test('strips YAML frontmatter — output is plain markdown', () => {
|
||||
const input = `---
|
||||
name: help
|
||||
description: Show help for GSD commands
|
||||
---
|
||||
|
||||
# GSD Help
|
||||
|
||||
Use \`/gsd-help\` to see available commands.
|
||||
`;
|
||||
|
||||
const result = convertClaudeCommandToCursorCommand(input);
|
||||
assert.ok(!result.startsWith('---'), 'cursor commands must not have YAML frontmatter');
|
||||
assert.ok(!result.includes('name: help'), 'name field must be stripped');
|
||||
assert.ok(!result.includes('description:'), 'description field must be stripped');
|
||||
assert.ok(result.includes('GSD Help'), 'body content must be preserved');
|
||||
});
|
||||
|
||||
test('applies convertClaudeToCursorMarkdown transforms (Bash → Shell, Claude Code → Cursor)', () => {
|
||||
const input = `---
|
||||
name: quick
|
||||
description: Quick task
|
||||
---
|
||||
|
||||
Use Bash( to run commands.
|
||||
This runs in Claude Code.
|
||||
`;
|
||||
|
||||
const result = convertClaudeCommandToCursorCommand(input);
|
||||
assert.ok(result.includes('Shell('), 'Bash( should be renamed to Shell(');
|
||||
assert.ok(!result.includes('Claude Code'), 'Claude Code brand reference should be replaced');
|
||||
assert.ok(result.includes('Cursor'), 'should reference Cursor instead');
|
||||
});
|
||||
|
||||
test('normalizes gsd: colon slash commands to gsd- hyphen form', () => {
|
||||
const input = `---
|
||||
name: plan-phase
|
||||
description: Plan a phase
|
||||
---
|
||||
|
||||
Next step: /gsd:execute-phase 17
|
||||
`;
|
||||
|
||||
const result = convertClaudeCommandToCursorCommand(input);
|
||||
assert.ok(result.includes('/gsd-execute-phase 17'), 'colon form should become hyphen form');
|
||||
assert.ok(!result.includes('/gsd:execute-phase'), 'colon form should be removed');
|
||||
});
|
||||
|
||||
test('handles input with no frontmatter gracefully', () => {
|
||||
const input = `# No Frontmatter Command
|
||||
|
||||
Some body content.
|
||||
`;
|
||||
|
||||
const result = convertClaudeCommandToCursorCommand(input);
|
||||
assert.ok(!result.startsWith('---'), 'output must not start with ---');
|
||||
assert.ok(result.includes('No Frontmatter Command'), 'body should be preserved');
|
||||
});
|
||||
|
||||
test('is exported from install.js', () => {
|
||||
assert.strictEqual(typeof convertClaudeCommandToCursorCommand, 'function',
|
||||
'convertClaudeCommandToCursorCommand must be exported from install.js');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,6 +131,31 @@ describe('installRuntimeArtifacts — gemini commands layout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('installRuntimeArtifacts — cursor commands layout (#785)', () => {
|
||||
test('cursor: skills/ AND commands/ both created; commands/gsd-help.md is plain markdown', (t) => {
|
||||
const configDir = createTempDir('gsd-ial-cursor-cmds-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
// Existing skills kind still present
|
||||
const skillsDir = path.join(configDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), 'skills/ must exist');
|
||||
assert.ok(fs.existsSync(path.join(skillsDir, 'gsd-help', 'SKILL.md')),
|
||||
'skills/gsd-help/SKILL.md must exist');
|
||||
|
||||
// New commands kind (#785)
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
assert.ok(fs.existsSync(commandsDir), 'commands/ must exist (#785)');
|
||||
assert.ok(fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
|
||||
'commands/gsd-help.md must exist (#785)');
|
||||
|
||||
// Cursor commands are plain markdown — no YAML frontmatter
|
||||
const helpContent = fs.readFileSync(path.join(commandsDir, 'gsd-help.md'), 'utf8');
|
||||
assert.ok(!helpContent.startsWith('---'), 'cursor commands must not start with YAML frontmatter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('installRuntimeArtifacts — cline no-op', () => {
|
||||
test('cline: no kinds — call succeeds, no dirs created', (t) => {
|
||||
const configDir = createTempDir('gsd-ial-cline-');
|
||||
|
||||
@@ -59,15 +59,23 @@ describe('resolveRuntimeArtifactLayout — claude global', () => {
|
||||
});
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — cursor', () => {
|
||||
test('returns correct layout for cursor', () => {
|
||||
test('returns correct layout for cursor — skills + commands kinds (#785)', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_DIR);
|
||||
assert.strictEqual(layout.runtime, 'cursor');
|
||||
assert.strictEqual(layout.configDir, FAKE_DIR);
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
assert.strictEqual(layout.kinds[0].kind, 'skills');
|
||||
assert.strictEqual(layout.kinds[0].destSubpath, 'skills');
|
||||
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
|
||||
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'must have a skills kind');
|
||||
assert.strictEqual(skillsKind.destSubpath, 'skills');
|
||||
assert.strictEqual(skillsKind.prefix, 'gsd-');
|
||||
assert.strictEqual(typeof skillsKind.stage, 'function');
|
||||
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'must have a commands kind (#785 Cursor 1.6 slash commands)');
|
||||
assert.strictEqual(commandsKind.destSubpath, 'commands');
|
||||
assert.strictEqual(commandsKind.prefix, 'gsd-');
|
||||
assert.strictEqual(typeof commandsKind.stage, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,6 +271,13 @@ describe('resolveRuntimeArtifactLayout edge-cases', () => {
|
||||
assert.ok(kindNames.includes('agents'), 'should have agents kind');
|
||||
});
|
||||
|
||||
test('cursor has both skills and commands kinds (#785)', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cursor', '/tmp/x');
|
||||
const kindNames = layout.kinds.map(k => k.kind);
|
||||
assert.ok(kindNames.includes('skills'), 'cursor must have skills kind');
|
||||
assert.ok(kindNames.includes('commands'), 'cursor must have commands kind (#785 Cursor 1.6)');
|
||||
});
|
||||
|
||||
test('claude global has only skills kind', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('claude', '/tmp/x', 'global');
|
||||
assert.strictEqual(layout.kinds.length, 1);
|
||||
@@ -394,3 +409,39 @@ describe('stage — opencode commands kind', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('stage — cursor commands kind (#785)', () => {
|
||||
test('cursor commands kind stage returns directory with converted .md files', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_STAGE_DIR);
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'cursor should have a commands kind (#785)');
|
||||
|
||||
const stagedDir = commandsKind.stage(PROFILE_CORE);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
|
||||
const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
||||
assert.ok(entries.length >= 1, 'at least one command file should be staged');
|
||||
|
||||
// Cursor commands are plain markdown — no YAML frontmatter
|
||||
for (const entry of entries) {
|
||||
const content = fs.readFileSync(path.join(stagedDir, entry), 'utf8');
|
||||
assert.ok(!content.startsWith('---'), `${entry}: cursor commands must not start with YAML frontmatter`);
|
||||
}
|
||||
});
|
||||
|
||||
test('cursor commands stage applies Cursor-specific content transforms', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_STAGE_DIR);
|
||||
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
|
||||
assert.ok(commandsKind, 'cursor should have a commands kind (#785)');
|
||||
|
||||
const stagedDir = commandsKind.stage(PROFILE_FULL);
|
||||
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
|
||||
|
||||
// Verify all staged files are .md only (no subdirectory SKILL.md layout)
|
||||
const entries = fs.readdirSync(stagedDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
assert.ok(entry.isFile(), `${entry.name}: cursor commands dir must contain only flat files`);
|
||||
assert.ok(entry.name.endsWith('.md'), `${entry.name}: must be .md file`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user