* 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:
6
.changeset/2644-cursor-deduplicate-menu.md
Normal file
6
.changeset/2644-cursor-deduplicate-menu.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2812
|
||||
---
|
||||
|
||||
**Cursor now shows each GSD workflow once in the slash menu while keeping skills available for contextual model invocation** — Upgrades safely retire manifest-managed legacy `commands/gsd-*.md` duplicates, back up modified managed copies, and preserve unknown user-authored commands.
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -158,6 +158,8 @@ build/
|
||||
/gsd-core/bin/lib/installer-migration-authoring.cjs
|
||||
/gsd-core/bin/lib/update-context.cjs
|
||||
/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs
|
||||
/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs
|
||||
/gsd-core/bin/lib/retired-artifact-cleanup.cjs
|
||||
/gsd-core/bin/lib/runtime-homes.cjs
|
||||
/gsd-core/bin/lib/model-catalog.cjs
|
||||
/gsd-core/bin/lib/configuration.cjs
|
||||
|
||||
@@ -2612,36 +2612,11 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
||||
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
||||
const adapter = getCursorSkillAdapterHeader(skillName);
|
||||
|
||||
// #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
|
||||
// menu (it defaults to true). Cursor also writes a commands/ surface (#785),
|
||||
// and surfacing both duplicated every /gsd-* entry. This mirrors the #789
|
||||
// CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
|
||||
// stay model-invocable background knowledge. (user-invocable:false hides from
|
||||
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\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();
|
||||
// Cursor skills are both slash-invocable and model-invocable. Do not emit the
|
||||
// unsupported `user-invocable` field: it is ignored by Cursor and previously
|
||||
// hid the real cause of duplicate entries, the parallel commands/ surface
|
||||
// retired in #2644.
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10621,8 +10596,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Descriptor-driven commands/ output report (#785 — Cursor 1.6 slash commands).
|
||||
// Gated by hostBehaviors.reportCommandsDir, not a hardcoded `isCursor` branch (#2089).
|
||||
// Descriptor-driven commands/ output report (currently CodeBuddy).
|
||||
// Cursor retired this parallel surface in #2644 because its skills are
|
||||
// already slash-menu entries as well as model-invocable context.
|
||||
if (_hostBehaviors(runtime).reportCommandsDir) {
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
if (fs.existsSync(commandsDir)) {
|
||||
@@ -13326,7 +13302,6 @@ module.exports = {
|
||||
applyRuntimeContentRewritesInPlace,
|
||||
getCodexSkillAdapterHeader,
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeCommandToCursorCommand,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
convertClaudeAgentToCodexAgent,
|
||||
generateCodexAgentToml,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"role": "runtime",
|
||||
"version": "1.9.1",
|
||||
"title": "Cursor",
|
||||
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"description": "Cursor IDE — skills-only workflow surface; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"tier": "core",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
@@ -55,14 +47,6 @@
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
@@ -107,7 +91,13 @@
|
||||
"frontmatterDialect": "cursor",
|
||||
"hooksJsonSurface": true,
|
||||
"skipSharedHooksInstall": true,
|
||||
"reportCommandsDir": true,
|
||||
"retiredArtifacts": [
|
||||
{
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"suffix": ".md"
|
||||
}
|
||||
],
|
||||
"skipUpdateBannerCommand": true,
|
||||
"skipSettingsUi": true,
|
||||
"managedHookEvents": [
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
"research-provider.cjs",
|
||||
"research-store.cjs",
|
||||
"resolution.cjs",
|
||||
"retired-artifact-cleanup.cjs",
|
||||
"review-lane-descriptor.cjs",
|
||||
"review-lane-invocation.cjs",
|
||||
"review-lane-runner.cjs",
|
||||
|
||||
@@ -508,6 +508,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `prompt-budget.cjs` | Pure token-budget accounting for review prompts — estimates tokens, applies deterministic trim priority (head-shrink PROJECT.md, proportional plan truncation, drop context/research/requirements, hard-fail guard), returns structured metadata for `review.max_prompt_tokens` (#3081) |
|
||||
| `research-provider.cjs` | Research provider waterfall, confidence tiers, and planResearch (cache-hits + fetch plan) |
|
||||
| `research-store.cjs` | Content-addressed research cache: sha256 keys, per-source TTL staleness, two-tier (user ~/.gsd / project .planning) store |
|
||||
| `retired-artifact-cleanup.cjs` | Manifest-safe cleanup for descriptor-declared retired runtime artifact surfaces; shared by install and profile/surface apply so removed layout kinds converge without deleting modified or unknown user files (#2644) |
|
||||
| `probe-core.cjs` | Generic spec-phase probe resolution model (compiled from `src/probe-core.cts`, gitignored; ADR-550 Decision 7) — the status×verification re-cut (`status: resolved/dismissed/unresolved` × per-probe `verification`), `validateResolution`/`validateRequirement`, `analyzeCoverage(items, resolutions?, validators)` merge/rollup/orphan-reject, the `byVerification` rollup, and the `runProbeCli` I/O scaffold; the shared seam consumed by `edge-probe` (and the prohibition probe #644); exports `VALID_STATUS`, `validateResolution`, `validateRequirement`, `analyzeCoverage`, `runProbeCli` (#550) |
|
||||
| `prohibition-enforcement.cjs` | Deterministic test-tier prohibition PRODUCER/gate (compiled from `src/prohibition-enforcement.cts`, gitignored; #1259, ADR-550 D5d "heavy half") — locates the wired mechanical check (`node-test` or `lint-rule`), confirms it is fail-first, runs it via an injectable runner, builds typed `enforcementEvidence`, and emits the `dispositionForProhibition` verdict; a passing wired check disposes green, a missing/failing/non-fail-first check hard-gates (flagged, non-green) in both interactive and autonomous modes; exports `runProhibitionEnforcement`, `routeProhibitionEnforcement`; CLI surface `gsd_run check prohibition-enforcement <request.json>` |
|
||||
| `review-lane-descriptor.cjs` | Declared reviewer-lane contract (compiled from `src/review-lane-descriptor.cts`, gitignored; ADR-2782) — the frozen `REVIEWER_LANES` roster, the lane slug grammar, and two pure parity gates: `checkReviewerLaneParity` (descriptor ↔ roster ↔ registry, plus anti-parity against re-added bespoke workflow legs) and `checkReviewerDocsParity` (declared flags and section titles ↔ `docs/COMMANDS.md`, `docs/FEATURES.md` and their locale mirrors; #2800, closes #2781/#2272); exports `REVIEWER_LANES`, `PARITY_VIOLATION`, `DOCS_PARITY_VIOLATION`, `LANE_SLUG_RE` |
|
||||
|
||||
@@ -282,7 +282,7 @@ COPILOT_CONFIG_DIR=~/.copilot-alt npx @opengsd/gsd-core@latest --copilot --globa
|
||||
npx @opengsd/gsd-core@latest --cursor --global
|
||||
```
|
||||
|
||||
Artifacts land in `~/.cursor/`. GSD installs slash commands (`~/.cursor/commands/gsd-*.md`), skills (`~/.cursor/skills/gsd-*/SKILL.md`), agents, and rule references. Each GSD action appears once in Cursor's `/` menu: the command surface is the single `/` entry point, and the skills are installed with `user-invocable: false` so they stay model-invocable background knowledge without duplicating the `/` entries.
|
||||
Artifacts land in `~/.cursor/`. GSD installs skills (`~/.cursor/skills/gsd-*/SKILL.md`), agents, and rule references. Cursor exposes each skill once in the `/` menu while keeping it available for contextual model invocation. Upgrading removes manifest-managed legacy `~/.cursor/commands/gsd-*.md` copies that previously duplicated those menu entries; unknown user-authored command files are preserved.
|
||||
|
||||
**Override the install directory:**
|
||||
|
||||
|
||||
@@ -505,6 +505,7 @@ Each row corresponds to one migration record in `src/installer-migrations/`.
|
||||
| `2026-07-17-opencode-baseline-commands-dir` | `005-opencode-baseline-commands-dir.cts` | 1.7.0 | global, local | No | Baselines pre-existing files under OpenCode's `commands/` (plural) directory during the first-time scan. #2329 moved OpenCode command materialization to `commands/`, but 000's `RUNTIME_SURFACES.opencode` is a shipped, immutable body that still only names the legacy `command/` alias, so this fix-forward migration widens the scanned surface. OpenCode only; Kilo is unaffected. |
|
||||
| `2026-07-20-pi-extension-cjs-to-js` | `006-pi-extension-cjs-to-js.cts` | 1.7.1 | global, local | Yes | Removes the stale `extensions/gsd.cjs` left by pre-#2470 pi installs. pi's extension auto-discovery (`isExtensionFile()`) accepts only `.ts`/`.js`, so the `.cjs` file was never loaded and `/gsd` never registered; #2470 renamed the installed artifact to `extensions/gsd.js`, orphaning the old path. Locally modified copies are backed up rather than deleted; an unmanifested `gsd.cjs` is preserved as a user file. pi only. |
|
||||
| `2026-07-28-retire-config-root-commonjs-marker` | `007-retire-config-root-commonjs-marker.cts` | 1.8.0 | global, local | Yes | Removes `<configRoot>/package.json` when it is exactly the `{"type":"commonjs"}` marker pre-#2544 installs wrote there. #2544 moved that marker into the directories GSD fills (`hooks/`, and the native plugin dir), so an upgraded install would otherwise keep both and stay pinned to CommonJS at a config root GSD no longer writes. Ownership is proven by exact content match, not the manifest (the marker was never manifest-recorded) — a `package.json` with any other content is left untouched, with no backup-and-remove branch. All runtimes; kimi's root marker lives outside `configDir` and is retired by the installer instead. |
|
||||
| `2026-07-29-cursor-retire-commands-surface` | `008-cursor-retire-commands-surface.cts` | 1.8.1 | global, local | Yes | Removes manifest-managed `commands/gsd-*.md` files from Cursor installs. Cursor already exposes the corresponding skills in the slash menu and to contextual model invocation, so the command copies produced duplicate entries (#2644). Modified files are backed up; unmanifested files are preserved. |
|
||||
|
||||
## Prior Art
|
||||
|
||||
|
||||
@@ -448,7 +448,7 @@ Sources consulted:
|
||||
- https://www.codebuddy.ai/docs/cli/settings
|
||||
- /websites/codebuddy_cn (Context7)
|
||||
|
||||
**EoS migration status (#2098):** Migrated onto the declarative adapter (dogfooded in `tests/declarative-reference-codebuddy.test.cjs`). The two remaining `isCodebuddy` branches in `bin/install.js` — a duplicate `commands/` slash-command output report, and a dead legacy agent-converter dispatch arm (unreachable since codebuddy is in `_DESCRIPTOR_AGENTS_RUNTIMES`) — were folded onto the already-generic `runtime.hostBehaviors.reportCommandsDir` (shared with Cursor) and removed outright; `isCodebuddy` no longer appears as a live read anywhere in `bin/install.js`, `src/runtime-artifact-conversion.cts`, `src/shell-command-projection.cts`, or `src/runtime-name-policy.cts`. Two upgrades land: (1) **extended hook events** — codebuddy's `extendedHookEvents` was previously `[]` (none wired); this PR wires all four — `SubagentStop`/`Stop`/`PreCompact`/`SubagentStart` — into `extendedHookEvents` (mirrors qwen/kimi), so an install now registers all four as hooks in `settings.json` alongside the pre-existing base session/tool events (`SessionStart`/`PreToolUse`/`PostToolUse`); cite https://www.codebuddy.ai/docs/cli/hooks. (2) **`dispatch.background`** — the descriptor already declared `true`, exceeding the `declarative-cli` profile baseline of `false`; the negotiation contract (`negotiateHostCapabilities`) now surfaces that value with no downgrade warning, documenting the legitimate deviation. Note: the CodeBuddy CLI has no background-dispatch frontmatter field on sub-agents (`agentMode`/`enabledAutoRun` are IDE-only per https://www.codebuddy.ai/docs/cli/sub-agents) — background dispatch remains a caller-side invocation parameter (`run_in_background: true`), not a field GSD's agent artifacts emit.
|
||||
**EoS migration status (#2098):** Migrated onto the declarative adapter (dogfooded in `tests/declarative-reference-codebuddy.test.cjs`). The two remaining `isCodebuddy` branches in `bin/install.js` — a duplicate `commands/` slash-command output report, and a dead legacy agent-converter dispatch arm (unreachable since codebuddy is in `_DESCRIPTOR_AGENTS_RUNTIMES`) — were folded onto the generic `runtime.hostBehaviors.reportCommandsDir` and removed outright; `isCodebuddy` no longer appears as a live read anywhere in `bin/install.js`, `src/runtime-artifact-conversion.cts`, `src/shell-command-projection.cts`, or `src/runtime-name-policy.cts`. Cursor also used that report flag historically, but retired its parallel command surface in #2644 because Cursor skills already appear in the slash menu. Two upgrades land: (1) **extended hook events** — codebuddy's `extendedHookEvents` was previously `[]` (none wired); this PR wires all four — `SubagentStop`/`Stop`/`PreCompact`/`SubagentStart` — into `extendedHookEvents` (mirrors qwen/kimi), so an install now registers all four as hooks in `settings.json` alongside the pre-existing base session/tool events (`SessionStart`/`PreToolUse`/`PostToolUse`); cite https://www.codebuddy.ai/docs/cli/hooks. (2) **`dispatch.background`** — the descriptor already declared `true`, exceeding the `declarative-cli` profile baseline of `false`; the negotiation contract (`negotiateHostCapabilities`) now surfaces that value with no downgrade warning, documenting the legitimate deviation. Note: the CodeBuddy CLI has no background-dispatch frontmatter field on sub-agents (`agentMode`/`enabledAutoRun` are IDE-only per https://www.codebuddy.ai/docs/cli/sub-agents) — background dispatch remains a caller-side invocation parameter (`run_in_background: true`), not a field GSD's agent artifacts emit.
|
||||
|
||||
---
|
||||
|
||||
@@ -848,4 +848,3 @@ hosts) adds a belt-and-suspenders `maxDepth: 5` ceiling independent of whatever
|
||||
engine enforces natively — there is no separate extension-side "subagent contribution"
|
||||
registration API beyond the chat participant + Language Model Tools already registered; VS Code's
|
||||
chat engine surfaces them to `#runSubagent` on its own.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ For the transform each converter applies, see [ADR-1593 §3 — converter transf
|
||||
| **codex** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToCodexSkill` | TOML config (`configFormat: toml`). Description truncated to 180 chars (`metadata.short-description`). `sandboxTier: codex-agent-sandbox`. |
|
||||
| **opencode** | `skills/` | `gsd-` | flat | recursive (`**` glob) | `convertClaudeCommandToOpencodeSkill` | XDG config home. Shares the opencode-family converter entry point (`convertClaudeCommandToOpencodeFamilySkill`). Also ships `command` (singular) commands. |
|
||||
| **kilo** | `skills/` | `gsd-` | flat | recursive (`**` glob) | `convertClaudeCommandToKiloSkill` | OpenCode fork; same `**` glob loader. `permissionWriter: kilo`. Also ships `command` commands. |
|
||||
| **cursor** | `skills/` | `gsd-` | flat | recursive | `convertClaudeCommandToCursorSkill` | Also ships flat `commands/` via `convertClaudeCommandToCursorCommand`. `configFormat: none`. |
|
||||
| **cursor** | `skills/` | `gsd-` | flat | recursive | `convertClaudeCommandToCursorSkill` | Skills are the single slash-menu and contextual invocation surface; legacy managed `commands/` copies are retired by migration 008 (#2644). `configFormat: none`. |
|
||||
| **copilot** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToCopilotSkill` | Markdown config. Scope-aware converter (global-home vs workspace-relative). |
|
||||
| **antigravity** | `skills/` | `gsd-` | flat | non-recursive (one-level) | `convertClaudeCommandToAntigravitySkill` | `dot-home-nested` config home. Scope-aware converter. Loader confirmed: *"will not recursive scan"*. Flattened by #1614 — `agy` scans only `skills/<name>/SKILL.md`, so nesting hid sub-skills. |
|
||||
| **windsurf** | — *(no skills kind)* | `gsd-` | — | workflows | `convertClaudeCommandToWindsurfWorkflow` | Emits `.windsurf/workflows/gsd-*.md` slash-command workflows. `configFormat: none`. `installSurface: profile-marker-only`. |
|
||||
|
||||
@@ -118,6 +118,8 @@ export default tseslint.config(
|
||||
'gsd-core/bin/lib/installer-migration-authoring.cjs',
|
||||
'gsd-core/bin/lib/update-context.cjs',
|
||||
'gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs',
|
||||
'gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs',
|
||||
'gsd-core/bin/lib/retired-artifact-cleanup.cjs',
|
||||
'gsd-core/bin/lib/runtime-homes.cjs',
|
||||
'gsd-core/bin/lib/model-catalog.cjs',
|
||||
'gsd-core/bin/lib/configuration.cjs',
|
||||
|
||||
@@ -1233,7 +1233,7 @@ const capabilities = {
|
||||
"role": "runtime",
|
||||
"version": "1.9.1",
|
||||
"title": "Cursor",
|
||||
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"description": "Cursor IDE — skills-only workflow surface; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"tier": "core",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
@@ -1259,14 +1259,6 @@ const capabilities = {
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
@@ -1285,14 +1277,6 @@ const capabilities = {
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
@@ -1337,7 +1321,13 @@ const capabilities = {
|
||||
"frontmatterDialect": "cursor",
|
||||
"hooksJsonSurface": true,
|
||||
"skipSharedHooksInstall": true,
|
||||
"reportCommandsDir": true,
|
||||
"retiredArtifacts": [
|
||||
{
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"suffix": ".md"
|
||||
}
|
||||
],
|
||||
"skipUpdateBannerCommand": true,
|
||||
"skipSettingsUi": true,
|
||||
"managedHookEvents": [
|
||||
@@ -5586,7 +5576,7 @@ const runtimes = {
|
||||
"role": "runtime",
|
||||
"version": "1.9.1",
|
||||
"title": "Cursor",
|
||||
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"description": "Cursor IDE — skills-only workflow surface; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
|
||||
"tier": "core",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
@@ -5612,14 +5602,6 @@ const runtimes = {
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
@@ -5638,14 +5620,6 @@ const runtimes = {
|
||||
"recursive": true,
|
||||
"converter": "convertClaudeCommandToCursorSkill"
|
||||
},
|
||||
{
|
||||
"kind": "commands",
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"nesting": "flat",
|
||||
"recursive": false,
|
||||
"converter": "convertClaudeCommandToCursorCommand"
|
||||
},
|
||||
{
|
||||
"kind": "agents",
|
||||
"destSubpath": "agents",
|
||||
@@ -5690,7 +5664,13 @@ const runtimes = {
|
||||
"frontmatterDialect": "cursor",
|
||||
"hooksJsonSurface": true,
|
||||
"skipSharedHooksInstall": true,
|
||||
"reportCommandsDir": true,
|
||||
"retiredArtifacts": [
|
||||
{
|
||||
"destSubpath": "commands",
|
||||
"prefix": "gsd-",
|
||||
"suffix": ".md"
|
||||
}
|
||||
],
|
||||
"skipUpdateBannerCommand": true,
|
||||
"skipSettingsUi": true,
|
||||
"managedHookEvents": [
|
||||
|
||||
@@ -707,7 +707,6 @@ const VALID_CONVERTER_NAMES = new Set([
|
||||
'convertClaudeCommandToCodebuddySkill',
|
||||
'convertClaudeCommandToCodexSkill',
|
||||
'convertClaudeCommandToCopilotSkill',
|
||||
'convertClaudeCommandToCursorCommand',
|
||||
'convertClaudeCommandToCursorSkill',
|
||||
'convertClaudeCommandToKiloSkill',
|
||||
'convertClaudeCommandToKimiSkill',
|
||||
|
||||
@@ -28,6 +28,7 @@ import runtimeArtifactInstallPlan = require('./runtime-artifact-install-plan.cjs
|
||||
import runtimeNamePolicy = require('./runtime-name-policy.cjs');
|
||||
import installProfiles = require('./install-profiles.cjs');
|
||||
import installerMigrations = require('./installer-migrations.cjs');
|
||||
import retiredArtifactCleanup = require('./retired-artifact-cleanup.cjs');
|
||||
import { posixNormalize } from './shell-command-projection.cjs';
|
||||
import { isPathConfined } from './external-descriptor-trust.cjs';
|
||||
import { ensureCommonJsMarker } from './commonjs-marker.cjs';
|
||||
@@ -725,6 +726,11 @@ function installRuntimeArtifacts(
|
||||
resolveAttribution: ResolveAttribution = () => undefined,
|
||||
capabilityRegistry?: any,
|
||||
): void {
|
||||
// A removed descriptor kind is no longer visited by the layout loop, so it
|
||||
// cannot prune its own previous output. Clean manifest-proven retired files
|
||||
// before materializing the current layout (#2644).
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
|
||||
|
||||
// Combined-family runtimes (OpenCode/Kilo, ADR-1239 / #2087): route through
|
||||
// the dedicated combined commands+skills+plugin orchestrator instead of the
|
||||
// generic layout-driven loop below, mirroring the bespoke install path that
|
||||
@@ -1304,6 +1310,12 @@ function installOpencodeFamilyArtifacts(
|
||||
* @param scope
|
||||
*/
|
||||
function uninstallRuntimeArtifacts(runtime: string, configDir: string, scope: string): void {
|
||||
// A retired descriptor kind is absent from the current uninstall plan, just
|
||||
// as it is absent from the install plan. Sweep manifest-proven output from
|
||||
// retired kinds before removing the current layout so a direct uninstall
|
||||
// cannot leave stale runtime surfaces behind (#2644).
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
|
||||
|
||||
// Legacy cleanup before layout-driven removal (scope-aware to avoid
|
||||
// removing Claude local commands/gsd/ which is the primary install dir).
|
||||
// Returns saved user artifacts so we can migrate AFTER layout removal
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Installer migration: retire Cursor's duplicate commands/ surface (#2644).
|
||||
*
|
||||
* Cursor discovers GSD skills as slash-menu entries while also keeping them
|
||||
* model-invocable. Older GSD releases installed the same workflows again as
|
||||
* commands/gsd-*.md, so every action appeared twice. The skills remain the
|
||||
* sole workflow surface; this migration removes only old command files proven
|
||||
* managed by gsd-file-manifest.json. Modified files are backed up first and
|
||||
* unmanifested files are preserved.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
interface ClassifiedArtifact {
|
||||
classification: string;
|
||||
}
|
||||
|
||||
interface MigrationAction {
|
||||
type: 'remove-managed' | 'backup-and-remove';
|
||||
relPath: string;
|
||||
reason: string;
|
||||
ownershipEvidence: string;
|
||||
}
|
||||
|
||||
interface MigrationPlanContext {
|
||||
configDir: string;
|
||||
classifyArtifact(relPath: string): ClassifiedArtifact;
|
||||
}
|
||||
|
||||
const COMMANDS_DIR = 'commands';
|
||||
const REASON =
|
||||
'Cursor exposes skills directly in the slash menu; the parallel command file duplicated the same GSD action (#2644)';
|
||||
const OWNERSHIP_EVIDENCE =
|
||||
'pre-#2644 Cursor installs record commands/gsd-*.md in gsd-file-manifest.json';
|
||||
|
||||
const migration = {
|
||||
id: '2026-07-29-cursor-retire-commands-surface',
|
||||
title: 'Retire Cursor duplicate commands surface',
|
||||
description:
|
||||
'Remove manifest-managed Cursor commands/gsd-*.md files now that skills are the single slash-menu and model-invocation surface.',
|
||||
introducedIn: '1.8.1',
|
||||
runtimes: ['cursor'],
|
||||
scopes: ['global', 'local'],
|
||||
destructive: true,
|
||||
plan: (ctx: MigrationPlanContext): MigrationAction[] => {
|
||||
const commandsDir = path.join(ctx.configDir, COMMANDS_DIR);
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
if (!fs.existsSync(commandsDir) || fs.lstatSync(commandsDir).isSymbolicLink()) return [];
|
||||
entries = fs.readdirSync(commandsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions: MigrationAction[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.startsWith('gsd-') || !entry.name.endsWith('.md')) continue;
|
||||
const relPath = path.posix.join(COMMANDS_DIR, entry.name);
|
||||
const artifact = ctx.classifyArtifact(relPath);
|
||||
if (artifact.classification === 'managed-pristine') {
|
||||
actions.push({ type: 'remove-managed', relPath, reason: REASON, ownershipEvidence: OWNERSHIP_EVIDENCE });
|
||||
} else if (artifact.classification === 'managed-modified') {
|
||||
actions.push({ type: 'backup-and-remove', relPath, reason: REASON, ownershipEvidence: OWNERSHIP_EVIDENCE });
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
},
|
||||
};
|
||||
|
||||
export = migration;
|
||||
102
src/retired-artifact-cleanup.cts
Normal file
102
src/retired-artifact-cleanup.cts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Conservative cleanup for artifact surfaces retired by a runtime descriptor.
|
||||
*
|
||||
* A descriptor may stop materializing an artifact kind while old installs
|
||||
* still contain files from that surface. Install and profile/surface apply
|
||||
* both call this helper so either path converges an existing installation.
|
||||
* Only files proven managed by the previous install manifest are removed;
|
||||
* modified and unknown files are preserved for the journaled installer
|
||||
* migration (or the user) to handle without data loss.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import installerMigrations = require('./installer-migrations.cjs');
|
||||
import { isPathConfined } from './external-descriptor-trust.cjs';
|
||||
|
||||
interface RetiredArtifactDescriptor {
|
||||
destSubpath?: unknown;
|
||||
prefix?: unknown;
|
||||
suffix?: unknown;
|
||||
}
|
||||
|
||||
interface CleanupResult {
|
||||
removed: string[];
|
||||
preserved: string[];
|
||||
}
|
||||
|
||||
interface RegistryShape {
|
||||
runtimes?: Record<string, {
|
||||
runtime?: {
|
||||
hostBehaviors?: {
|
||||
retiredArtifacts?: RetiredArtifactDescriptor[];
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
// Generated at build time, so no TypeScript source declaration exists.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const capabilityRegistry = require('./capability-registry.cjs') as unknown as RegistryShape;
|
||||
|
||||
function retiredArtifactsFor(runtime: string): RetiredArtifactDescriptor[] {
|
||||
const configured = capabilityRegistry.runtimes?.[runtime]?.runtime?.hostBehaviors?.retiredArtifacts;
|
||||
return Array.isArray(configured) ? configured : [];
|
||||
}
|
||||
|
||||
function pruneRetiredRuntimeArtifacts(runtime: string, configDir: string): CleanupResult {
|
||||
const result: CleanupResult = { removed: [], preserved: [] };
|
||||
const declarations = retiredArtifactsFor(runtime);
|
||||
if (declarations.length === 0) return result;
|
||||
|
||||
const manifest = installerMigrations.readInstallManifest(configDir);
|
||||
for (const declaration of declarations) {
|
||||
const destSubpath = declaration.destSubpath;
|
||||
const prefix = declaration.prefix;
|
||||
const suffix = declaration.suffix;
|
||||
if (
|
||||
typeof destSubpath !== 'string' || destSubpath.length === 0 ||
|
||||
typeof prefix !== 'string' || prefix.length === 0 ||
|
||||
typeof suffix !== 'string' || suffix.length === 0 ||
|
||||
!isPathConfined(destSubpath, configDir)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const destDir = path.resolve(configDir, destSubpath);
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
if (!fs.existsSync(destDir) || fs.lstatSync(destDir).isSymbolicLink()) continue;
|
||||
entries = fs.readdirSync(destDir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith(suffix)) continue;
|
||||
const relPath = path.posix.join(destSubpath.replace(/\\/g, '/'), entry.name);
|
||||
const artifact = installerMigrations.classifyArtifact(configDir, relPath, manifest);
|
||||
if (artifact.classification !== 'managed-pristine') {
|
||||
result.preserved.push(relPath);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(path.join(destDir, entry.name));
|
||||
result.removed.push(relPath);
|
||||
} catch {
|
||||
result.preserved.push(relPath);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.readdirSync(destDir).length === 0) fs.rmdirSync(destDir);
|
||||
} catch {
|
||||
// Non-empty, unreadable, or concurrently changed: preserve the directory.
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export = { pruneRetiredRuntimeArtifacts, retiredArtifactsFor };
|
||||
@@ -981,36 +981,11 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
|
||||
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
||||
const adapter = getCursorSkillAdapterHeader(skillName);
|
||||
|
||||
// #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
|
||||
// menu (it defaults to true). Cursor also writes a commands/ surface (#785),
|
||||
// and surfacing both duplicated every /gsd-* entry. This mirrors the #789
|
||||
// CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
|
||||
// stay model-invocable background knowledge. (user-invocable:false hides from
|
||||
// '/' while keeping model invocation — distinct from disable-model-invocation.)
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\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();
|
||||
// Cursor skills are both slash-invocable and model-invocable. Do not emit the
|
||||
// unsupported `user-invocable` field: it is ignored by Cursor and previously
|
||||
// hid the real cause of duplicate entries, the parallel commands/ surface
|
||||
// retired in #2644.
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||
}
|
||||
|
||||
// --- Windsurf converters ---
|
||||
@@ -3030,7 +3005,6 @@ export = {
|
||||
buildKimiAgentArtifacts,
|
||||
convertClaudeToCursorMarkdown,
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeCommandToCursorCommand,
|
||||
convertClaudeToWindsurfMarkdown,
|
||||
convertClaudeCommandToWindsurfSkill,
|
||||
convertClaudeCommandToWindsurfWorkflow,
|
||||
|
||||
@@ -52,6 +52,8 @@ const { findInstallSourceRoot } = runtimeArtifactLayout;
|
||||
import runtimeArtifactConversion = require('./runtime-artifact-conversion.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import runtimeArtifactInstallPlan = require('./runtime-artifact-install-plan.cjs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import retiredArtifactCleanup = require('./retired-artifact-cleanup.cjs');
|
||||
const { assertDestWithinConfigHome } = runtimeArtifactInstallPlan;
|
||||
|
||||
const SURFACE_FILE_NAME = '.gsd-surface.json';
|
||||
@@ -351,6 +353,9 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
|
||||
}
|
||||
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
|
||||
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
|
||||
// Profile toggles must converge retired surfaces too. Once a kind disappears
|
||||
// from artifactLayout there is no normal sync pass left to prune it (#2644).
|
||||
retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(layout.runtime, layout.configDir);
|
||||
// #1575: agents kind now mirrors createRuntimeArtifactInstallPlan — build
|
||||
// agentCtx (pathPrefix + attribution) and pass it to kind.stage() so
|
||||
// stageAgentsForRuntimeWithConverter applies the full inline-loop pipeline
|
||||
|
||||
@@ -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