* fix(#2644): deduplicate Cursor slash menu * fix: preserve installer executable mode * chore: add changeset for PR #2812 * test(#2644): acknowledge Cursor emission changes * test(#2644): drop spent emitted drift acknowledgments * fix(#2644): remove retired Cursor command converter --------- Co-authored-by: clezcoding <clezcoding@users.noreply.github.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -23,6 +23,7 @@ const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
@@ -40,6 +41,8 @@ const {
|
||||
resolveRuntimeArtifactLayout,
|
||||
} = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
|
||||
|
||||
const { applySurface } = require('../gsd-core/bin/lib/surface.cjs');
|
||||
|
||||
const {
|
||||
loadSkillsManifest,
|
||||
resolveProfile,
|
||||
@@ -105,13 +108,10 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod
|
||||
});
|
||||
t.after(restore);
|
||||
|
||||
// #1928: gemini (the only runtime whose 'commands' kind used a
|
||||
// namespaced-by-dir 'commands/gsd' layout) was removed; cursor is the
|
||||
// stand-in — it has a 'commands' kind but prefixes files 'gsd-'
|
||||
// (destSubpath basename !== prefix stem), so proof.md → gsd-proof.md.
|
||||
installer.installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE);
|
||||
// Augment has a flat commands kind and prefixes proof.md as gsd-proof.md.
|
||||
installer.installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
assert.strictEqual(planArgs.layout.runtime, 'cursor');
|
||||
assert.strictEqual(planArgs.layout.runtime, 'augment');
|
||||
assert.strictEqual(planArgs.layout.configDir, configDir);
|
||||
assert.strictEqual(planArgs.layout.scope, 'global');
|
||||
assert.strictEqual(planArgs.resolvedProfile, RESOLVED_CORE);
|
||||
@@ -288,28 +288,19 @@ describe('installRuntimeArtifacts — hermes nested layout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('installRuntimeArtifacts — cursor commands layout (#785)', () => {
|
||||
test('cursor: skills/ AND commands/ both created; commands/gsd-help.md is plain markdown', (t) => {
|
||||
describe('installRuntimeArtifacts — cursor skills-only layout (#2644)', () => {
|
||||
test('cursor: skills/ is created and commands/ is not materialized', (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');
|
||||
assert.ok(!fs.existsSync(path.join(configDir, 'commands')),
|
||||
'Cursor must not materialize commands/ because skills already populate the slash menu');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -750,9 +741,9 @@ describe('uninstallRuntimeArtifacts — consumes Runtime Artifact Uninstall Plan
|
||||
});
|
||||
t.after(restore);
|
||||
|
||||
installer.uninstallRuntimeArtifacts('cursor', configDir, 'global');
|
||||
installer.uninstallRuntimeArtifacts('augment', configDir, 'global');
|
||||
|
||||
assert.strictEqual(planLayout.runtime, 'cursor');
|
||||
assert.strictEqual(planLayout.runtime, 'augment');
|
||||
assert.strictEqual(planLayout.configDir, configDir);
|
||||
assert.strictEqual(planLayout.scope, 'global');
|
||||
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')));
|
||||
@@ -2494,17 +2485,9 @@ describe('enh-789 — installRuntimeArtifacts codebuddy emits commands and skill
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #2341: extend the #789 de-dup to Cursor ─────────────────────────────────
|
||||
// Cursor installs BOTH a skills surface and a commands surface (#785/#803), and
|
||||
// surfaces both in its '/' menu — so every /gsd-* appeared twice. The #789 fix
|
||||
// (skills user-invocable:false → model-invocable but out of '/') was scoped to
|
||||
// CodeBuddy only and never applied to Cursor. Cursor honors the same SKILL.md
|
||||
// `user-invocable` convention (verified: user-invocable:false hides a skill from
|
||||
// '/' while keeping it model-invocable, distinct from disable-model-invocation).
|
||||
// Fix: emit Cursor skills with user-invocable:false so commands are the single
|
||||
// '/' entry point.
|
||||
describe('fix-2341 — Cursor skills marked user-invocable:false', () => {
|
||||
test('convertClaudeCommandToCursorSkill emits user-invocable: false', () => {
|
||||
// ─── #2644: Cursor skills are the one slash + model surface ──────────────────
|
||||
describe('fix-2644 — Cursor has one menu entry per GSD workflow', () => {
|
||||
test('convertClaudeCommandToCursorSkill emits only supported invocation metadata', () => {
|
||||
const src = [
|
||||
'---',
|
||||
'name: gsd:help',
|
||||
@@ -2515,33 +2498,93 @@ describe('fix-2341 — Cursor skills marked user-invocable:false', () => {
|
||||
'',
|
||||
].join('\n');
|
||||
const out = convertClaudeCommandToCursorSkill(src, 'gsd-help');
|
||||
assert.ok(/^user-invocable:\s*false\s*$/m.test(out),
|
||||
`Cursor SKILL.md frontmatter must hide skill from '/' menu (user-invocable: false). Got:\n${out}`);
|
||||
assert.ok(!/^user-invocable:/m.test(out),
|
||||
`Cursor does not support user-invocable; the field must not be emitted. Got:\n${out}`);
|
||||
assert.ok(!/^disable-model-invocation:/m.test(out),
|
||||
'Cursor skill must remain available for contextual model invocation');
|
||||
});
|
||||
|
||||
test('installed cursor skills/gsd-help/SKILL.md is hidden from the / menu', (t) => {
|
||||
const configDir = createTempDir('gsd-fix2341-skillhide-');
|
||||
test('fresh install keeps the skill slash/model surface and omits commands', (t) => {
|
||||
const configDir = createTempDir('gsd-fix2644-fresh-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
const skill = fs.readFileSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md'), 'utf8');
|
||||
assert.ok(/^user-invocable:\s*false\s*$/m.test(skill),
|
||||
'installed Cursor SKILL.md must set user-invocable: false so it is not a duplicate / entry');
|
||||
assert.ok(!/^user-invocable:/m.test(skill));
|
||||
assert.ok(!/^disable-model-invocation:/m.test(skill));
|
||||
assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')));
|
||||
});
|
||||
|
||||
test('cursor still installs the commands surface (the single / entry point)', (t) => {
|
||||
const configDir = createTempDir('gsd-fix2341-cmd-');
|
||||
test('reinstall removes manifest-proven legacy commands and preserves unknown files', (t) => {
|
||||
const configDir = createTempDir('gsd-fix2644-upgrade-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const managedContent = '# old generated help\n';
|
||||
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), managedContent);
|
||||
fs.writeFileSync(path.join(commandsDir, 'gsd-my-custom.md'), '# user command\n');
|
||||
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
|
||||
version: '1.8.0',
|
||||
timestamp: '2026-07-28T00:00:00.000Z',
|
||||
mode: 'full',
|
||||
files: {
|
||||
'commands/gsd-help.md': crypto.createHash('sha256').update(managedContent).digest('hex'),
|
||||
},
|
||||
}));
|
||||
|
||||
installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE);
|
||||
|
||||
// The commands surface stays user-invocable — de-dup hides the skill, not the command.
|
||||
assert.ok(fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')),
|
||||
'commands/gsd-help.md (the / entry point) must still be installed');
|
||||
const cmd = fs.readFileSync(path.join(configDir, 'commands', 'gsd-help.md'), 'utf8');
|
||||
assert.ok(!/^user-invocable:\s*false\s*$/m.test(cmd),
|
||||
'the command surface must remain user-invocable (only the skill is hidden)');
|
||||
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
|
||||
'manifest-proven legacy command must be retired');
|
||||
assert.ok(fs.existsSync(path.join(commandsDir, 'gsd-my-custom.md')),
|
||||
'unmanifested user command must be preserved');
|
||||
});
|
||||
|
||||
test('profile surface apply also retires a manifest-proven legacy command', (t) => {
|
||||
const configDir = createTempDir('gsd-fix2644-surface-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const content = '# old generated help\n';
|
||||
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), content);
|
||||
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
|
||||
version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'core',
|
||||
files: { 'commands/gsd-help.md': crypto.createHash('sha256').update(content).digest('hex') },
|
||||
}));
|
||||
|
||||
const layout = resolveRuntimeArtifactLayout('cursor', configDir, 'global');
|
||||
applySurface(configDir, layout, MANIFEST);
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
|
||||
'profile toggles must not leave or recreate the retired duplicate surface');
|
||||
assert.ok(fs.existsSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md')));
|
||||
});
|
||||
|
||||
test('uninstall also retires manifest-proven legacy commands', (t) => {
|
||||
const configDir = createTempDir('gsd-fix2644-uninstall-');
|
||||
t.after(() => cleanup(configDir));
|
||||
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const managedContent = '# old generated help\n';
|
||||
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), managedContent);
|
||||
fs.writeFileSync(path.join(commandsDir, 'user-command.md'), '# user command\n');
|
||||
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
|
||||
version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'full',
|
||||
files: {
|
||||
'commands/gsd-help.md': crypto.createHash('sha256').update(managedContent).digest('hex'),
|
||||
},
|
||||
}));
|
||||
|
||||
uninstallRuntimeArtifacts('cursor', configDir, 'global');
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
|
||||
'direct uninstall must remove a manifest-proven retired command');
|
||||
assert.ok(fs.existsSync(path.join(commandsDir, 'user-command.md')),
|
||||
'direct uninstall must preserve unknown user commands');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user