fix(install): apply path replacement in copyCommandsAsClaudeSkills (#1677)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
'---',
|
||||
'',
|
||||
'<execution_context>',
|
||||
'@~/.claude/get-shit-done/workflows/manager.md',
|
||||
'@~/.claude/get-shit-done/references/ui-brand.md',
|
||||
'</execution_context>',
|
||||
].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', () => {
|
||||
|
||||
Reference in New Issue
Block a user