Files
get-shit-done/scripts/fix-slash-commands.cjs
Patrick Clery f9c1f01971 fix: extend fix-slash-commands SEARCH_DIRS to agents/, sdk/src/, .clinerules
scripts/fix-slash-commands.cjs SEARCH_DIRS did not cover agents/, sdk/src/,
or top-level files, so 9 colon-form references survived in 6 files. The hit
at agents/gsd-codebase-mapper.md:105 propagated into ~/.claude/agents/ at
install time (the fixer is not wired into install) and produced unrunnable
/gsd:<cmd> suggestions in agent output on non-Gemini runtimes.

This commit includes Pass 1 (the 9 line edits) AND Pass 2 (extending the
fixer's SEARCH_DIRS so future regressions are auto-rewritten and caught by
the bug-2543 guard, which mirrors that list). The standalone bug-3100 test
added in the prior revision is removed in favor of the bug-2543 guard's
extended scan, per CONTRIBUTING.md test standards (no source-grep tests on
non-.md files).

Refs #3100
2026-05-05 13:19:10 -04:00

107 lines
3.8 KiB
JavaScript

'use strict';
/**
* One-shot script: replace retired /gsd:<cmd> with /gsd-<cmd> for known command names.
* Only replaces when followed by a word boundary (space, newline, quote, backtick, ), end).
*
* The transform is exported as a pure function so it can be unit-tested directly
* (see tests/bug-2543-gsd-slash-namespace.test.cjs) without needing fixture files.
*/
const fs = require('node:fs');
const path = require('node:path');
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const SEARCH_DIRS = [
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib'),
path.join(__dirname, '..', 'get-shit-done', 'workflows'),
path.join(__dirname, '..', 'get-shit-done', 'references'),
path.join(__dirname, '..', 'get-shit-done', 'templates'),
path.join(__dirname, '..', 'get-shit-done', 'contexts'),
path.join(__dirname, '..', 'commands', 'gsd'),
path.join(__dirname, '..', 'agents'),
path.join(__dirname, '..', 'sdk', 'src'),
];
const TOP_LEVEL_FILES = [
path.join(__dirname, '..', '.clinerules'),
];
const SKIP_DIRS = new Set(['node_modules', 'dist', '.turbo']);
const EXTENSIONS = new Set(['.md', '.cjs', '.js', '.ts', '.tsx']);
// Test files contain intentional fixture strings (e.g. inputs the sanitizer
// is expected to strip). Rewriting them changes test semantics.
function isTestFile(name) {
return /\.test\.(c?js|tsx?)$/.test(name);
}
function buildPattern(cmdNames) {
// Empty input would compile `/gsd:()(?=[^a-zA-Z0-9_-]|$)/g`, which the regex
// engine still matches at any `/gsd:` token followed by a non-word boundary
// (e.g. EOL, whitespace, punctuation) — rewriting it to a stray `/gsd-`.
// Short-circuit so the caller can no-op on a missing/empty registry rather
// than perform an unintended broad rewrite.
if (!Array.isArray(cmdNames) || cmdNames.length === 0) return null;
const sorted = [...cmdNames].sort((a, b) => b.length - a.length); // longest first to avoid partial matches
return new RegExp(`/gsd:(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`, 'g');
}
/**
* Pure transform: rewrite retired `/gsd:<cmd>` to `/gsd-<cmd>` for the given command names.
* Returns the rewritten string. Identifiers not in `cmdNames` (e.g. `/gsd:sdk`,
* `/gsd:tools`) are left untouched.
*/
function transformContent(src, cmdNames) {
const pattern = buildPattern(cmdNames);
if (!pattern) return src;
return src.replace(pattern, (_, cmd) => `/gsd-${cmd}`);
}
function readCmdNames() {
return fs.readdirSync(COMMANDS_DIR)
.filter(f => f.endsWith('.md'))
.map(f => f.replace(/\.md$/, ''));
}
function processFile(file, cmdNames) {
const pattern = buildPattern(cmdNames);
if (!pattern) return;
let src;
try { src = fs.readFileSync(file, 'utf-8'); } catch { return; }
const replaced = transformContent(src, cmdNames);
if (replaced !== src) {
fs.writeFileSync(file, replaced, 'utf-8');
const count = (src.match(pattern) || []).length;
console.log(` ${count} replacements: ${path.relative(path.join(__dirname, '..'), file)}`);
}
}
function processDir(dir, cmdNames) {
const pattern = buildPattern(cmdNames);
if (!pattern) return;
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) {
if (SKIP_DIRS.has(e.name)) continue;
processDir(full, cmdNames);
} else if (EXTENSIONS.has(path.extname(e.name)) && !isTestFile(e.name)) {
processFile(full, cmdNames);
}
}
}
if (require.main === module) {
const cmdNames = readCmdNames();
for (const dir of SEARCH_DIRS) {
processDir(dir, cmdNames);
}
for (const file of TOP_LEVEL_FILES) {
processFile(file, cmdNames);
}
console.log('Done.');
}
module.exports = { transformContent, buildPattern, SKIP_DIRS };