Spaces:
Runtime error
Runtime error
File size: 6,024 Bytes
a6b96c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | 'use strict';
/**
* One-shot script + library: bidirectional GSD slash-command namespace normalizer.
*
* - Default direction (transformContent): retired /gsd-<cmd> → /gsd:<cmd>
* (keeps monorepo sources, docs, and workflows in the active colon form).
* - Reverse direction (transformContentToHyphen): /gsd:<cmd> / gsd:<cmd> → gsd-<cmd>
* (used during skill installation for runtimes that register skills under the
* canonical hyphen form established in #2808).
*
* Both directions only rewrite known commands from `commands/gsd/*.md` (longest-first
* matching + word-boundary safety). Non-commands (gsd-sdk, gsd-tools, etc.) are
* intentionally left untouched.
*
* The transforms are pure and exported for use by the installer and tests.
*/
const fs = require('node:fs');
const path = require('node:path');
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const SEARCH_DIRS = [
path.join(__dirname, '..', 'gsd-core', 'bin', 'lib'),
path.join(__dirname, '..', 'gsd-core', 'workflows'),
path.join(__dirname, '..', 'gsd-core', 'references'),
path.join(__dirname, '..', 'gsd-core', 'templates'),
path.join(__dirname, '..', 'gsd-core', 'contexts'),
path.join(__dirname, '..', 'commands', 'gsd'),
path.join(__dirname, '..', 'agents'),
path.join(__dirname, '..', 'hooks'),
];
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}`);
}
/**
* Build regex for the reverse direction (colon form → hyphen form).
* Matches both "gsd:cmd" and "/gsd:cmd" (the leading / is preserved automatically
* because it is not part of the match). Uses longest-first ordering plus
* bidirectional word-boundary safety (negative lookbehind on the left, lookahead
* on the right) so matches only occur at token boundaries.
*/
function buildColonPattern(cmdNames) {
if (!Array.isArray(cmdNames) || cmdNames.length === 0) return null;
const sorted = [...cmdNames].sort((a, b) => b.length - a.length);
return new RegExp(`(?<![a-zA-Z0-9_-])gsd:(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`, 'g');
}
/**
* Pure transform (reverse): rewrite `/gsd:<cmd>` / `gsd:<cmd>` to hyphen form
* for known GSD commands.
*
* Non-command identifiers (e.g. gsd-sdk, gsd-tools) are left untouched, matching
* the safety contract of the forward transform.
*/
function transformContentToHyphen(src, cmdNames) {
const pattern = buildColonPattern(cmdNames);
if (!pattern) return src;
return src.replace(pattern, (_, cmd) => `gsd-${cmd}`);
}
function readCmdNames() {
try {
return fs.readdirSync(COMMANDS_DIR)
.filter(f => f.endsWith('.md'))
.map(f => f.replace(/\.md$/, ''));
} catch (err) {
// Only swallow the missing-directory case. Any other error (EACCES, ENOTDIR,
// etc.) indicates a real misconfiguration and must propagate so callers are
// not silently handed an empty registry while the real problem goes undetected.
if (err.code !== 'ENOENT') throw err;
// COMMANDS_DIR may not exist on installs that use skill-based runtimes or
// global Claude installs (no local commands/gsd/ directory). Return [] so
// callers that handle an empty array gracefully (buildPattern returns null,
// transformContent is a no-op) are not broken by a missing directory.
return [];
}
}
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,
transformContentToHyphen,
buildPattern,
buildColonPattern,
readCmdNames,
SKIP_DIRS
};
|