From 718580354399c24a5a3fe51c158ad02ab6b1d490 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 4 Apr 2026 15:12:58 -0400 Subject: [PATCH] fix(install): apply path replacement in copyCommandsAsClaudeSkills (#1677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: drop Windows runner, add static hardcoded-path detection Replace the Windows CI runner with a static analysis test that catches the same class of platform-specific path bugs (C:\, /home/, /Users/, /tmp/) without requiring an actual Windows machine. - tests/hardcoded-paths.test.cjs: new static scanner that checks string literals in all source JS/CJS files for hardcoded platform paths; runs on Linux/macOS in <100ms and fires on every PR - .github/workflows/test.yml: remove windows-latest from matrix; switch macOS smoke-test runner from Node 22 → Node 24 (the declared standard) - package.json: bump engines.node from >=20.0.0 to >=22.0.0 (Node 20 reached EOL April 2026) Matrix goes from 4 runners → 3 runners per run: ubuntu/22 ubuntu/24 macos/24 Co-Authored-By: Claude Sonnet 4.6 * fix(install): apply path replacement in copyCommandsAsClaudeSkills (#1653) copyCommandsAsClaudeSkills received pathPrefix as a parameter but never used it — all 51 SKILL.md files kept hardcoded ~/.claude/ paths even on local (per-project) installs, causing every skill's @-file references to resolve to a nonexistent global directory. Add the same three regex replacements that copyCommandsAsCodexSkills already applies: ~/.claude/ → pathPrefix, $HOME/.claude/ → pathPrefix, ./.claude/ → ./getDirName(runtime)/. Closes #1653 Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- bin/install.js | 3 ++ tests/claude-skills-migration.test.cjs | 75 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/bin/install.js b/bin/install.js index ffca354d7..0348c5abd 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3740,6 +3740,9 @@ function copyCommandsAsClaudeSkills(srcDir, skillsDir, prefix, pathPrefix, runti fs.mkdirSync(skillDir, { recursive: true }); let content = fs.readFileSync(srcPath, 'utf8'); + content = content.replace(/~\/\.claude\//g, pathPrefix); + content = content.replace(/\$HOME\/\.claude\//g, pathPrefix); + content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`); content = processAttribution(content, getCommitAttribution('claude')); content = convertClaudeCommandToClaudeSkill(content, skillName); diff --git a/tests/claude-skills-migration.test.cjs b/tests/claude-skills-migration.test.cjs index b1ed405db..ceec0392b 100644 --- a/tests/claude-skills-migration.test.cjs +++ b/tests/claude-skills-migration.test.cjs @@ -272,6 +272,81 @@ describe('copyCommandsAsClaudeSkills', () => { }); }); +// ─── Path replacement in Claude skills (#1653) ──────────────────────────────── + +describe('copyCommandsAsClaudeSkills path replacement (#1653)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-claude-path-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + test('replaces ~/.claude/ paths with pathPrefix on local install', () => { + const srcDir = path.join(tmpDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync( + path.join(srcDir, 'manager.md'), + [ + '---', + 'name: gsd:manager', + 'description: Manager command', + '---', + '', + '', + '@~/.claude/get-shit-done/workflows/manager.md', + '@~/.claude/get-shit-done/references/ui-brand.md', + '', + ].join('\n') + ); + + const skillsDir = path.join(tmpDir, 'skills'); + const localPrefix = '/Users/test/myproject/.claude/'; + copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', localPrefix, 'claude', false); + + const content = fs.readFileSync(path.join(skillsDir, 'gsd-manager', 'SKILL.md'), 'utf8'); + assert.ok(!content.includes('~/.claude/'), 'no hardcoded ~/.claude/ paths remain'); + assert.ok(content.includes(localPrefix + 'get-shit-done/workflows/manager.md'), 'path rewritten to local prefix'); + assert.ok(content.includes(localPrefix + 'get-shit-done/references/ui-brand.md'), 'reference path rewritten'); + }); + + test('replaces $HOME/.claude/ paths with pathPrefix', () => { + const srcDir = path.join(tmpDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync( + path.join(srcDir, 'debug.md'), + '---\nname: gsd:debug\ndescription: Debug\n---\n\n@$HOME/.claude/get-shit-done/workflows/debug.md' + ); + + const skillsDir = path.join(tmpDir, 'skills'); + const localPrefix = '/tmp/project/.claude/'; + copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', localPrefix, 'claude', false); + + const content = fs.readFileSync(path.join(skillsDir, 'gsd-debug', 'SKILL.md'), 'utf8'); + assert.ok(!content.includes('$HOME/.claude/'), 'no $HOME/.claude/ paths remain'); + assert.ok(content.includes(localPrefix + 'get-shit-done/workflows/debug.md'), 'path rewritten'); + }); + + test('global install preserves $HOME/.claude/ when pathPrefix matches', () => { + const srcDir = path.join(tmpDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + fs.writeFileSync( + path.join(srcDir, 'next.md'), + '---\nname: gsd:next\ndescription: Next\n---\n\n@~/.claude/get-shit-done/workflows/next.md' + ); + + const skillsDir = path.join(tmpDir, 'skills'); + copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.claude/', 'claude', true); + + const content = fs.readFileSync(path.join(skillsDir, 'gsd-next', 'SKILL.md'), 'utf8'); + assert.ok(content.includes('$HOME/.claude/get-shit-done/workflows/next.md'), 'global paths use $HOME form'); + assert.ok(!content.includes('~/.claude/'), '~/ form replaced with $HOME/ form'); + }); +}); + // ─── Legacy cleanup during install ────────────────────────────────────────── describe('Legacy commands/gsd/ cleanup', () => {