* 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:
@@ -4041,9 +4041,9 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
|
||||
// ─── 25. ADR-857 phase 5e: closed ConverterName enum (Part B) ─────────────────
|
||||
|
||||
describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
|
||||
test('VALID_CONVERTER_NAMES has exactly 27 entries (17 command/skill/workflow + 10 agent converters)', () => {
|
||||
test('VALID_CONVERTER_NAMES has exactly 26 entries (16 command/skill/workflow + 10 agent converters)', () => {
|
||||
assert.ok(VALID_CONVERTER_NAMES instanceof Set, 'VALID_CONVERTER_NAMES must be a Set');
|
||||
assert.strictEqual(VALID_CONVERTER_NAMES.size, 27, 'VALID_CONVERTER_NAMES must have exactly 27 entries, got: ' + VALID_CONVERTER_NAMES.size);
|
||||
assert.strictEqual(VALID_CONVERTER_NAMES.size, 26, 'VALID_CONVERTER_NAMES must have exactly 26 entries, got: ' + VALID_CONVERTER_NAMES.size);
|
||||
});
|
||||
|
||||
test('VALID_CONVERTER_NAMES contains all expected converter names', () => {
|
||||
@@ -4057,7 +4057,6 @@ describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
|
||||
'convertClaudeCommandToCodebuddySkill',
|
||||
'convertClaudeCommandToCodexSkill',
|
||||
'convertClaudeCommandToCopilotSkill',
|
||||
'convertClaudeCommandToCursorCommand',
|
||||
'convertClaudeCommandToCursorSkill',
|
||||
'convertClaudeCommandToKiloSkill',
|
||||
'convertClaudeCommandToKimiSkill',
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
* 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';
|
||||
@@ -17,7 +14,6 @@ const assert = require('node:assert/strict');
|
||||
const {
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
convertClaudeCommandToCursorCommand,
|
||||
_applyRuntimeRewrites,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
@@ -85,74 +81,6 @@ Planner body
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 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');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── _applyRuntimeRewrites(cursor) — bare-form regression (#1356) ────────────
|
||||
//
|
||||
// Prior to this fix the cursor branch only rewrote trailing-slash ~/.claude/
|
||||
|
||||
@@ -108,7 +108,11 @@ test('cursor descriptor declares runtime.hostBehaviors (the folded-in behaviors)
|
||||
assert.equal(hb.frontmatterDialect, 'cursor');
|
||||
assert.equal(hb.hooksJsonSurface, true);
|
||||
assert.equal(hb.skipSharedHooksInstall, true);
|
||||
assert.equal(hb.reportCommandsDir, true);
|
||||
assert.deepEqual(hb.retiredArtifacts, [
|
||||
{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' },
|
||||
]);
|
||||
assert.equal(hb.reportCommandsDir, undefined,
|
||||
'Cursor no longer installs a parallel commands/ surface (#2644)');
|
||||
assert.ok(Array.isArray(hb.managedHookEvents) && hb.managedHookEvents.length >= 6,
|
||||
'managedHookEvents must list at least 6 events (AC4a)');
|
||||
});
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
* already descriptor-driven except for two residual `isCodebuddy` branches
|
||||
* in bin/install.js:
|
||||
* 1. a duplicate `commands/` slash-command output report (byte-identical
|
||||
* to the generic `hostBehaviors.reportCommandsDir` block already used
|
||||
* by Cursor) — folded onto `hostBehaviors.reportCommandsDir` and deleted.
|
||||
* to the generic `hostBehaviors.reportCommandsDir` block) — folded onto
|
||||
* `hostBehaviors.reportCommandsDir` and deleted. Cursor retired that
|
||||
* parallel commands surface later in #2644.
|
||||
* 2. a dead legacy agent-converter dispatch arm in the inline agent-copy
|
||||
* loop, unreachable because codebuddy is a member of
|
||||
* `_DESCRIPTOR_AGENTS_RUNTIMES` (installRuntimeArtifacts already wrote
|
||||
|
||||
147
tests/emitted-drift-acks/2812-cursor-single-menu-surface.json
Normal file
147
tests/emitted-drift-acks/2812-cursor-single-menu-surface.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"commands/gsd-add-tests.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ai-integration-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-audit-fix.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-audit-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-audit-uat.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-autonomous.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-capture.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-cleanup.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-code-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-complete-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-config.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-debug.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-discuss-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-docs-update.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-eval-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-execute-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-explore.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-extract-learnings.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-fast.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-forensics.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-graphify.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-health.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-help.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-import.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-inbox.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ingest-docs.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-manager.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-map-codebase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-mempalace-capture.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-mempalace-recall.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-milestone-summary.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-mvp-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-new-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-new-project.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-next.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ns-context.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ns-ideate.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ns-manage.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ns-project.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ns-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ns-workflow.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-onboard.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-pause-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-plan-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-plan-review-convergence.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-pr-branch.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-profile-user.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-progress.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-quick.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-resume-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-review-backlog.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-secure-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-settings.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ship.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-sketch.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-spec-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-spike.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-stats.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-surface.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-thread.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ui-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ui-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-ultraplan-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-undo.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-update.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-validate-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-verify-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-workspace.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"commands/gsd-workstreams.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
|
||||
"skills/gsd-add-tests/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ai-integration-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-audit-fix/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-audit-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-audit-uat/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-autonomous/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-cleanup/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-code-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-complete-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-config/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-debug/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-discuss-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-docs-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-eval-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-execute-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-explore/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-extract-learnings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-fast/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-forensics/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-graphify/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-health/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-help/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-import/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-inbox/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ingest-docs/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-manager/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-map-codebase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-mempalace-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-mempalace-recall/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-milestone-summary/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-mvp-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-new-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-new-project/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-next/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ns-context/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ns-ideate/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ns-manage/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ns-project/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ns-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ns-workflow/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-onboard/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-pause-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-plan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-plan-review-convergence/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-pr-branch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-profile-user/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-progress/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-quick/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-resume-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-review-backlog/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-secure-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-settings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ship/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-sketch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-spec-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-spike/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-stats/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-surface/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-thread/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ui-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ui-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-ultraplan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-undo/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-validate-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-verify-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-workspace/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
|
||||
"skills/gsd-workstreams/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644)."
|
||||
}
|
||||
}
|
||||
71
tests/fixtures/install-tree/cursor.json
vendored
71
tests/fixtures/install-tree/cursor.json
vendored
@@ -35,77 +35,6 @@
|
||||
"agents/gsd-ui-researcher.md",
|
||||
"agents/gsd-user-profiler.md",
|
||||
"agents/gsd-verifier.md",
|
||||
"commands/gsd-add-tests.md",
|
||||
"commands/gsd-ai-integration-phase.md",
|
||||
"commands/gsd-audit-fix.md",
|
||||
"commands/gsd-audit-milestone.md",
|
||||
"commands/gsd-audit-uat.md",
|
||||
"commands/gsd-autonomous.md",
|
||||
"commands/gsd-capture.md",
|
||||
"commands/gsd-cleanup.md",
|
||||
"commands/gsd-code-review.md",
|
||||
"commands/gsd-complete-milestone.md",
|
||||
"commands/gsd-config.md",
|
||||
"commands/gsd-debug.md",
|
||||
"commands/gsd-discuss-phase.md",
|
||||
"commands/gsd-docs-update.md",
|
||||
"commands/gsd-eval-review.md",
|
||||
"commands/gsd-execute-phase.md",
|
||||
"commands/gsd-explore.md",
|
||||
"commands/gsd-extract-learnings.md",
|
||||
"commands/gsd-fast.md",
|
||||
"commands/gsd-forensics.md",
|
||||
"commands/gsd-graphify.md",
|
||||
"commands/gsd-health.md",
|
||||
"commands/gsd-help.md",
|
||||
"commands/gsd-import.md",
|
||||
"commands/gsd-inbox.md",
|
||||
"commands/gsd-ingest-docs.md",
|
||||
"commands/gsd-manager.md",
|
||||
"commands/gsd-map-codebase.md",
|
||||
"commands/gsd-mempalace-capture.md",
|
||||
"commands/gsd-mempalace-recall.md",
|
||||
"commands/gsd-milestone-summary.md",
|
||||
"commands/gsd-mvp-phase.md",
|
||||
"commands/gsd-new-milestone.md",
|
||||
"commands/gsd-new-project.md",
|
||||
"commands/gsd-next.md",
|
||||
"commands/gsd-ns-context.md",
|
||||
"commands/gsd-ns-ideate.md",
|
||||
"commands/gsd-ns-manage.md",
|
||||
"commands/gsd-ns-project.md",
|
||||
"commands/gsd-ns-review.md",
|
||||
"commands/gsd-ns-workflow.md",
|
||||
"commands/gsd-onboard.md",
|
||||
"commands/gsd-pause-work.md",
|
||||
"commands/gsd-phase.md",
|
||||
"commands/gsd-plan-phase.md",
|
||||
"commands/gsd-plan-review-convergence.md",
|
||||
"commands/gsd-pr-branch.md",
|
||||
"commands/gsd-profile-user.md",
|
||||
"commands/gsd-progress.md",
|
||||
"commands/gsd-quick.md",
|
||||
"commands/gsd-resume-work.md",
|
||||
"commands/gsd-review-backlog.md",
|
||||
"commands/gsd-review.md",
|
||||
"commands/gsd-secure-phase.md",
|
||||
"commands/gsd-settings.md",
|
||||
"commands/gsd-ship.md",
|
||||
"commands/gsd-sketch.md",
|
||||
"commands/gsd-spec-phase.md",
|
||||
"commands/gsd-spike.md",
|
||||
"commands/gsd-stats.md",
|
||||
"commands/gsd-surface.md",
|
||||
"commands/gsd-thread.md",
|
||||
"commands/gsd-ui-phase.md",
|
||||
"commands/gsd-ui-review.md",
|
||||
"commands/gsd-ultraplan-phase.md",
|
||||
"commands/gsd-undo.md",
|
||||
"commands/gsd-update.md",
|
||||
"commands/gsd-validate-phase.md",
|
||||
"commands/gsd-verify-work.md",
|
||||
"commands/gsd-workspace.md",
|
||||
"commands/gsd-workstreams.md",
|
||||
"gsd-core/.gsd-runtime",
|
||||
"gsd-core/VERSION",
|
||||
"gsd-core/bin/check-latest-version.cjs",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1682,6 +1682,9 @@ test('shipped installer-migration checksums are locked to a committed baseline (
|
||||
// never manifest-recorded — so the action declares its own classification.
|
||||
'2026-07-28-retire-config-root-commonjs-marker':
|
||||
'sha256:8f2140cbe8f2dd8f7dfd52a0f6957c5edfe966c52d7e6e4d74ec7366930e0e1d',
|
||||
// Migration 008: retire Cursor's duplicate commands/ surface (#2644).
|
||||
'2026-07-29-cursor-retire-commands-surface':
|
||||
'sha256:d0b2b812a3f752650f2518b48280f74a5937c80ec8412bac493382dfa3db083f',
|
||||
};
|
||||
|
||||
const { DEFAULT_MIGRATIONS_DIR, migrationChecksum: computeChecksum } = require('../gsd-core/bin/lib/installer-migrations.cjs');
|
||||
@@ -1794,6 +1797,76 @@ test('reconciles a drifted applied-migration checksum into install state on appl
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cursor duplicate commands-surface retirement (#2644)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
const cursorCommandsMigration = require('../gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs');
|
||||
|
||||
test('migration 008 plans only manifest-managed gsd-*.md command files', (t) => {
|
||||
const configDir = createTempInstall();
|
||||
t.after(() => cleanup(configDir));
|
||||
writeFile(configDir, 'commands/gsd-help.md', '# help\n');
|
||||
writeFile(configDir, 'commands/gsd-custom.md', '# custom\n');
|
||||
writeFile(configDir, 'commands/not-gsd.md', '# other\n');
|
||||
|
||||
const actions = cursorCommandsMigration.plan({
|
||||
configDir,
|
||||
classifyArtifact(relPath) {
|
||||
if (relPath === 'commands/gsd-help.md') return { classification: 'managed-pristine' };
|
||||
return { classification: 'unknown' };
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(actions.map((action) => [action.type, action.relPath]), [
|
||||
['remove-managed', 'commands/gsd-help.md'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('migration 008 backs up a modified managed command and preserves an unknown neighbor', (t) => {
|
||||
const configDir = createTempInstall();
|
||||
t.after(() => cleanup(configDir));
|
||||
writeFile(configDir, 'commands/gsd-help.md', '# locally modified help\n');
|
||||
writeFile(configDir, 'commands/gsd-custom.md', '# user command\n');
|
||||
writeManifest(configDir, {
|
||||
'commands/gsd-help.md': sha256('# original generated help\n'),
|
||||
});
|
||||
|
||||
const result = runInstallerMigrations({
|
||||
configDir, runtime: 'cursor', scope: 'global', migrations: [cursorCommandsMigration],
|
||||
now: () => '2026-07-29T00:00:00.000Z',
|
||||
});
|
||||
|
||||
assert.equal(result.plan.actions[0].type, 'backup-and-remove');
|
||||
assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')));
|
||||
assert.ok(fs.existsSync(path.join(configDir, 'commands', 'gsd-custom.md')),
|
||||
'unmanifested user command must be preserved');
|
||||
const journal = JSON.parse(fs.readFileSync(path.join(configDir, result.journalRelPath), 'utf8'));
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(configDir, journal.actions[0].backupRelPath), 'utf8'),
|
||||
'# locally modified help\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('migration 008 is scoped to Cursor for both global and local installs', (t) => {
|
||||
for (const scope of ['global', 'local']) {
|
||||
const configDir = createTempInstall();
|
||||
t.after(() => cleanup(configDir));
|
||||
writeFile(configDir, 'commands/gsd-help.md', '# help\n');
|
||||
writeManifest(configDir, { 'commands/gsd-help.md': sha256('# help\n') });
|
||||
const result = runInstallerMigrations({
|
||||
configDir, runtime: 'cursor', scope, migrations: [cursorCommandsMigration],
|
||||
now: () => `2026-07-29T00:00:0${scope === 'global' ? '0' : '1'}.000Z`,
|
||||
});
|
||||
assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')),
|
||||
`${scope} Cursor install must retire the duplicate command`);
|
||||
assert.deepEqual(result.appliedMigrationIds, ['2026-07-29-cursor-retire-commands-surface']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/bug-3357-codex-legacy-hooks-json-migration.test.cjs — consolidation epic #1969 (B5 #1974)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
155
tests/retired-artifact-cleanup.test.cjs
Normal file
155
tests/retired-artifact-cleanup.test.cjs
Normal file
@@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const retiredArtifactCleanup = require('../gsd-core/bin/lib/retired-artifact-cleanup.cjs');
|
||||
const capabilityRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
|
||||
const cursorBehaviors = capabilityRegistry.runtimes.cursor.runtime.hostBehaviors;
|
||||
|
||||
function createFixture(t) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-retired-cleanup-'));
|
||||
const configDir = path.join(root, 'config');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
t.after(() => {
|
||||
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- isolated temp fixture cleanup
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
return { root, configDir };
|
||||
}
|
||||
|
||||
function setDeclarations(t, declarations) {
|
||||
const original = cursorBehaviors.retiredArtifacts;
|
||||
cursorBehaviors.retiredArtifacts = declarations;
|
||||
t.after(() => {
|
||||
cursorBehaviors.retiredArtifacts = original;
|
||||
});
|
||||
}
|
||||
|
||||
function writeFile(root, relPath, content) {
|
||||
const filePath = path.join(root, relPath);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function sha256(content) {
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
function writeManifest(configDir, files) {
|
||||
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,
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
test('rejects malformed and escaping retired-artifact declarations', (t) => {
|
||||
const { root, configDir } = createFixture(t);
|
||||
const outsideFile = writeFile(root, 'escape/gsd-help.md', '# outside\n');
|
||||
const localFile = writeFile(configDir, 'commands/gsd-help.md', '# local\n');
|
||||
setDeclarations(t, [
|
||||
{ destSubpath: null, prefix: 'gsd-', suffix: '.md' },
|
||||
{ destSubpath: '../escape', prefix: 'gsd-', suffix: '.md' },
|
||||
]);
|
||||
|
||||
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
|
||||
|
||||
assert.deepEqual(result, { removed: [], preserved: [] });
|
||||
assert.ok(fs.existsSync(outsideFile), 'an escaping destination must never be touched');
|
||||
assert.ok(fs.existsSync(localFile), 'a malformed declaration must not affect valid-looking files');
|
||||
});
|
||||
|
||||
test('refuses a symlinked retired destination directory', (t) => {
|
||||
const { root, configDir } = createFixture(t);
|
||||
const targetDir = path.join(root, 'outside-commands');
|
||||
const targetFile = writeFile(targetDir, 'gsd-help.md', '# outside\n');
|
||||
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
||||
fs.symlinkSync(targetDir, path.join(configDir, 'commands'), linkType);
|
||||
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
|
||||
|
||||
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
|
||||
|
||||
assert.deepEqual(result, { removed: [], preserved: [] });
|
||||
assert.ok(fs.existsSync(targetFile), 'cleanup must not follow the destination symlink');
|
||||
});
|
||||
|
||||
test('preserves and reports a user-modified managed file', (t) => {
|
||||
const { configDir } = createFixture(t);
|
||||
const relPath = 'commands/gsd-help.md';
|
||||
const originalContent = '# generated\n';
|
||||
const modifiedContent = '# user changed this\n';
|
||||
const filePath = writeFile(configDir, relPath, modifiedContent);
|
||||
writeManifest(configDir, { [relPath]: sha256(originalContent) });
|
||||
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
|
||||
|
||||
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
|
||||
|
||||
assert.deepEqual(result, { removed: [], preserved: [relPath] });
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), modifiedContent);
|
||||
assert.ok(fs.existsSync(path.dirname(filePath)), 'directory must remain while a preserved file exists');
|
||||
});
|
||||
|
||||
test('removes and reports a managed-pristine file and then removes the empty directory', (t) => {
|
||||
const { configDir } = createFixture(t);
|
||||
const relPath = 'commands/gsd-help.md';
|
||||
const content = '# generated\n';
|
||||
const filePath = writeFile(configDir, relPath, content);
|
||||
const commandsDir = path.dirname(filePath);
|
||||
writeManifest(configDir, { [relPath]: sha256(content) });
|
||||
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
|
||||
|
||||
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
|
||||
|
||||
assert.deepEqual(result, { removed: [relPath], preserved: [] });
|
||||
assert.ok(!fs.existsSync(filePath));
|
||||
assert.ok(!fs.existsSync(commandsDir), 'an empty retired destination should be removed');
|
||||
});
|
||||
|
||||
test('leaves non-matching files untouched and keeps their directory', (t) => {
|
||||
const { configDir } = createFixture(t);
|
||||
const managedRelPath = 'commands/gsd-help.md';
|
||||
const managedContent = '# generated\n';
|
||||
const managedFile = writeFile(configDir, managedRelPath, managedContent);
|
||||
const unrelatedFile = writeFile(configDir, 'commands/user-command.md', '# user\n');
|
||||
writeManifest(configDir, { [managedRelPath]: sha256(managedContent) });
|
||||
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
|
||||
|
||||
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
|
||||
|
||||
assert.deepEqual(result, { removed: [managedRelPath], preserved: [] });
|
||||
assert.ok(!fs.existsSync(managedFile));
|
||||
assert.ok(fs.existsSync(unrelatedFile));
|
||||
assert.ok(fs.existsSync(path.dirname(unrelatedFile)), 'a non-empty destination must remain');
|
||||
});
|
||||
|
||||
test('reports a managed-pristine file as preserved when unlinkSync fails', (t) => {
|
||||
const { configDir } = createFixture(t);
|
||||
const relPath = 'commands/gsd-help.md';
|
||||
const content = '# generated\n';
|
||||
const filePath = writeFile(configDir, relPath, content);
|
||||
writeManifest(configDir, { [relPath]: sha256(content) });
|
||||
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
|
||||
|
||||
const originalUnlinkSync = fs.unlinkSync;
|
||||
fs.unlinkSync = (candidate) => {
|
||||
if (path.resolve(candidate) === path.resolve(filePath)) throw new Error('injected unlink failure');
|
||||
return originalUnlinkSync(candidate);
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
|
||||
} finally {
|
||||
fs.unlinkSync = originalUnlinkSync;
|
||||
}
|
||||
|
||||
assert.deepEqual(result, { removed: [], preserved: [relPath] });
|
||||
assert.ok(fs.existsSync(filePath), 'a failed unlink must leave the file in place');
|
||||
});
|
||||
@@ -61,14 +61,13 @@ const GOLDEN = {
|
||||
// Old switch: BOTH scopes returned [skills, commands] (no scope branch).
|
||||
// 5b backfill: local == global.
|
||||
// ADR-1235 step 1 (#1763): agents kind added.
|
||||
// #2644: commands are deliberately retired so Cursor exposes a single skills surface.
|
||||
'cursor/global': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
{ kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' },
|
||||
],
|
||||
'cursor/local': [
|
||||
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
|
||||
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
|
||||
{ kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' },
|
||||
],
|
||||
|
||||
|
||||
@@ -65,11 +65,11 @@ describe('resolveRuntimeArtifactLayout — claude global', () => {
|
||||
});
|
||||
|
||||
describe('resolveRuntimeArtifactLayout — cursor', () => {
|
||||
test('returns correct layout for cursor — skills + commands + agents kinds (#785, ADR-1235)', () => {
|
||||
test('returns correct layout for cursor — skills + agents only (#2644)', () => {
|
||||
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_DIR);
|
||||
assert.strictEqual(layout.runtime, 'cursor');
|
||||
assert.strictEqual(layout.configDir, FAKE_DIR);
|
||||
assert.strictEqual(layout.kinds.length, 3);
|
||||
assert.strictEqual(layout.kinds.length, 2);
|
||||
|
||||
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
|
||||
assert.ok(skillsKind, 'must have a skills kind');
|
||||
@@ -77,11 +77,8 @@ describe('resolveRuntimeArtifactLayout — cursor', () => {
|
||||
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');
|
||||
assert.equal(layout.kinds.find(k => k.kind === 'commands'), undefined,
|
||||
'Cursor skills are the sole slash-menu surface; commands would duplicate them (#2644)');
|
||||
|
||||
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
|
||||
assert.ok(agentsKind, 'must have an agents kind (ADR-1235 §1 descriptor cutover)');
|
||||
@@ -416,11 +413,11 @@ describe('resolveRuntimeArtifactLayout edge-cases', () => {
|
||||
assert.ok(kindNames.includes('agents'), 'should have agents kind');
|
||||
});
|
||||
|
||||
test('cursor has both skills and commands kinds (#785)', () => {
|
||||
test('cursor has a skills kind and no commands kind (#2644)', () => {
|
||||
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)');
|
||||
assert.ok(!kindNames.includes('commands'), 'cursor commands kind would duplicate skill menu entries');
|
||||
});
|
||||
|
||||
test('claude global has only skills kind', () => {
|
||||
@@ -657,39 +654,11 @@ describe('stage — opencode/kilo skills kind (#784)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('stage — cursor commands kind (#785)', () => {
|
||||
test('cursor commands kind stage returns directory with converted .md files', () => {
|
||||
describe('stage — cursor retired commands kind (#2644)', () => {
|
||||
test('cursor layout exposes no commands staging surface', () => {
|
||||
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`);
|
||||
}
|
||||
assert.equal(commandsKind, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user