From 08e42b0ef107aad0a68caa451f463ca0c7e52868 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 17 Jun 2026 08:51:48 -0400 Subject: [PATCH] fix(#1356): rewrite bare ~/.claude paths in the Cursor install branch (#1368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#1356): rewrite bare ~/.claude paths in the Cursor install branch The Cursor branch of _applyRuntimeRewrites only rewrote the trailing-slash .claude forms, so bare ~/.claude / $HOME/.claude references survived into installed Cursor artifacts (skills/gsd-surface, skills/gsd-graphify, workflows/plan-phase, workflows/autonomous), tripping the post-install "unreplaced .claude path reference(s)" audit. Same regression class as #983/#2418/#2545 — every other affected branch was patched; cursor was missed. - Add the three bare-form rewrites (~/.claude, $HOME/.claude, ./.claude) to the cursor branch, mirroring cline/trae/augment/codebuddy. They run after the slash forms (no double-replace) and use (?![\w-]) so .claude-plugin / .claudeignore are not corrupted. - SKILL.md content also passes through this stage, so no stage-1 converter change is needed. Verified: 40 bare refs in the real leaking files → 0. Co-Authored-By: Claude Opus 4.8 * chore(#1356): add changeset for Cursor bare-path rewrite fix Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/1356-cursor-bare-claude-paths.md | 6 ++ bin/install.js | 6 ++ tests/cursor-conversion.test.cjs | 110 ++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 .changeset/1356-cursor-bare-claude-paths.md diff --git a/.changeset/1356-cursor-bare-claude-paths.md b/.changeset/1356-cursor-bare-claude-paths.md new file mode 100644 index 000000000..6fe8ffbe9 --- /dev/null +++ b/.changeset/1356-cursor-bare-claude-paths.md @@ -0,0 +1,6 @@ +--- +type: Fixed +pr: 1368 +--- + +**`gsd install --cursor` no longer leaves bare `~/.claude` paths in installed artifacts** — the Cursor install branch only rewrote the trailing-slash `.claude` forms, so bare `~/.claude` / `$HOME/.claude` references survived into installed skills and workflows (e.g. `gsd-surface`, `gsd-graphify`, `plan-phase`, `autonomous`) and tripped the post-install "unreplaced .claude path reference(s)" warning, pointing at a directory that doesn't exist on a Cursor-only install. The Cursor branch now rewrites bare forms too (mirroring the Trae/Augment/Copilot branches), using a `(?![\w-])` lookahead so `.claude-plugin` / `.claudeignore` are not corrupted. (#1356) diff --git a/bin/install.js b/bin/install.js index f084958ef..8b982f426 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6792,6 +6792,12 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) { content = content.replace(/~\/\.claude\//g, pathPrefix); content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); content = content.replace(/\.\/\.claude\//g, `./${dirName}/`); + // Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that + // .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary + // fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin). + content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix); + content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix); + content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`); content = content.replace(/~\/\.cursor\//g, pathPrefix); content = processAttribution(content, getCommitAttribution(runtime)); break; diff --git a/tests/cursor-conversion.test.cjs b/tests/cursor-conversion.test.cjs index 92a961a31..131cfd2ba 100644 --- a/tests/cursor-conversion.test.cjs +++ b/tests/cursor-conversion.test.cjs @@ -18,6 +18,7 @@ const { convertClaudeCommandToCursorSkill, convertClaudeAgentToCursorAgent, convertClaudeCommandToCursorCommand, + _applyRuntimeRewrites, } = require('../bin/install.js'); describe('convertClaudeCommandToCursorSkill', () => { @@ -151,3 +152,112 @@ Some body content. '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/ +// and $HOME/.claude/ forms. Bare end-of-token references (end of line, +// inside backtick spans, before punctuation) survived and triggered the +// post-install audit "Found N unreplaced .claude path reference(s)". +// +// Fix: add three bare-form rewrites (mirroring augment/windsurf/trae) using +// (?![\w-]) to avoid corrupting .claude-plugin / .claudeignore. +// +// TDD proof: these assertions FAIL before the fix and PASS after. + +describe('_applyRuntimeRewrites(cursor) — bare-form ~/.claude regression (#1356)', () => { + const CURSOR_PATH_PREFIX = '~/.cursor/'; + + // Compound input that exercises every bare and slash ~/.claude / $HOME/.claude + // form the fix must handle. .claude-plugin is intentionally excluded here + // because \b fires before the hyphen — its preservation is tested separately. + const COMPOUND_INPUT = [ + 'Config dir: ~/.claude', + 'Also: $HOME/.claude', + 'Slash form: ~/.claude/gsd-core/foo.md', + 'Inline: paths `~/.claude`, `~/.cursor`', + ].join('\n'); + + // Input for the preservation test only — includes .claude-plugin. + const COMPOUND_INPUT_WITH_PLUGIN = COMPOUND_INPUT + '\nPlugin installed at: ~/.claude-plugin/plugin.json'; + + test('bare ~/.claude at end of line is rewritten (no trailing slash)', () => { + const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'cursor', CURSOR_PATH_PREFIX); + assert.ok( + !/~\/\.claude\b/.test(result), + `bare ~/.claude must be gone; got:\n${result}`, + ); + assert.ok(result.includes('~/.cursor'), `cursor prefix must appear; got:\n${result}`); + }); + + test('bare $HOME/.claude at end of line is rewritten', () => { + const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'cursor', CURSOR_PATH_PREFIX); + assert.ok( + !/\$HOME\/\.claude\b/.test(result), + `bare $HOME/.claude must be gone; got:\n${result}`, + ); + }); + + test('bare ~/.claude inside a code span (before punctuation) is rewritten', () => { + const input = 'paths: `~/.claude`, `~/.cursor`'; + const result = _applyRuntimeRewrites(input, 'cursor', CURSOR_PATH_PREFIX); + assert.ok( + !/~\/\.claude\b/.test(result), + `bare ~/.claude before punctuation must be gone; got:\n${result}`, + ); + }); + + test('trailing-slash ~/.claude/gsd-core/foo.md is rewritten exactly once (no doubling)', () => { + const input = '~/.claude/gsd-core/foo.md'; + const result = _applyRuntimeRewrites(input, 'cursor', CURSOR_PATH_PREFIX); + assert.ok(result.includes('~/.cursor/gsd-core/foo.md'), `slash form must be rewritten; got: ${result}`); + assert.ok(!result.includes('cursor/cursor'), `path must not be doubled; got: ${result}`); + assert.ok(!result.includes('.claude'), `no .claude must survive; got: ${result}`); + }); + + test('zero surviving bare ~/.claude or $HOME/.claude refs in compound input', () => { + const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'cursor', CURSOR_PATH_PREFIX); + const bareClaudePattern = /(?:~|\$HOME)\/\.claude\b/; + assert.ok( + !bareClaudePattern.test(result), + `no bare ~/.claude / $HOME/.claude must survive; got:\n${result}`, + ); + }); + + test('~/.claude-plugin is NOT corrupted — (?![\\w-]) lookahead preserves it', () => { + const result = _applyRuntimeRewrites(COMPOUND_INPUT_WITH_PLUGIN, 'cursor', CURSOR_PATH_PREFIX); + assert.ok( + result.includes('~/.claude-plugin'), + `~/.claude-plugin must be preserved; got:\n${result}`, + ); + assert.ok( + !result.includes('~/.cursor-plugin'), + `~/.cursor-plugin must NOT appear; got:\n${result}`, + ); + }); + + test('bare relative ./.claude (end-of-token) is rewritten to ./.cursor', () => { + // The relative bare form ./.claude (no trailing slash) must also be caught. + // End-of-line, inside a code span, and before punctuation variants. + // The cursor dir name derived from pathPrefix '~/.cursor/' is '.cursor', + // so ./.claude → ./.cursor (the ./${dotDirName} form). + const pathPrefix = '~/.cursor/'; + const isGlobal = false; + + const input = [ + 'see ./.claude for config', + 'also `./.claude` in a code span', + ].join('\n'); + + const result = _applyRuntimeRewrites(input, 'cursor', pathPrefix, isGlobal); + + assert.ok( + result.includes('./.cursor'), + `bare ./.claude must be rewritten to ./.cursor; got:\n${result}`, + ); + assert.ok( + !/\/\.claude\b/.test(result), + `no bare ./.claude (end-of-token) must survive; got:\n${result}`, + ); + }); +});