Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
160 lines
5.9 KiB
JavaScript
160 lines
5.9 KiB
JavaScript
'use strict';
|
|
/**
|
|
* One-shot script + library: bidirectional MSD slash-command namespace normalizer.
|
|
*
|
|
* - Default direction (transformContent): retired /msd-<cmd> → /msd:<cmd>
|
|
* (keeps monorepo sources, docs, and workflows in the active colon form).
|
|
* - Reverse direction (transformContentToHyphen): /msd:<cmd> / msd:<cmd> → msd-<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/msd/*.md` (longest-first
|
|
* matching + word-boundary safety). Non-commands (msd-sdk, msd-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', 'msd');
|
|
const SEARCH_DIRS = [
|
|
path.join(__dirname, '..', 'msd-core', 'bin', 'lib'),
|
|
path.join(__dirname, '..', 'msd-core', 'workflows'),
|
|
path.join(__dirname, '..', 'msd-core', 'references'),
|
|
path.join(__dirname, '..', 'msd-core', 'templates'),
|
|
path.join(__dirname, '..', 'msd-core', 'contexts'),
|
|
path.join(__dirname, '..', 'commands', 'msd'),
|
|
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 `/msd-()(?=[^a-zA-Z0-9_-]|$)/g`, which the regex
|
|
// engine still matches at any `/msd-` token followed by a non-word boundary
|
|
// (e.g. EOL, whitespace, punctuation) — rewriting it to a stray `/msd:`.
|
|
// 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(`/msd-(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`, 'g');
|
|
}
|
|
|
|
/**
|
|
* Pure transform: rewrite retired `/msd-<cmd>` to `/msd:<cmd>` for the given command names.
|
|
* Returns the rewritten string. Identifiers not in `cmdNames` (e.g. `/msd-sdk`,
|
|
* `/msd-tools`) are left untouched.
|
|
*/
|
|
function transformContent(src, cmdNames) {
|
|
const pattern = buildPattern(cmdNames);
|
|
if (!pattern) return src;
|
|
return src.replace(pattern, (_, cmd) => `/msd:${cmd}`);
|
|
}
|
|
|
|
/**
|
|
* Build regex for the reverse direction (colon form → hyphen form).
|
|
* Matches both "msd:cmd" and "/msd: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_-])msd:(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`, 'g');
|
|
}
|
|
|
|
/**
|
|
* Pure transform (reverse): rewrite `/msd:<cmd>` / `msd:<cmd>` to hyphen form
|
|
* for known MSD commands.
|
|
*
|
|
* Non-command identifiers (e.g. msd-sdk, msd-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) => `msd-${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/msd/ 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
|
|
};
|