CodeRabbit surfaced one outstanding inline finding plus an outside-diff finding and a finer point on two already-remediated sites; all addressed: 1. (inline, Minor) runtime-slash.cjs:31 — a degenerate input like `/gsd:`, `gsd:`, or `gsd-` normalizes to empty and the previous fallback returned the original colon-form input, reintroducing the deprecated shape the module exists to suppress. Now returns `''` (empty string) so callers see "no command" instead of an unroutable string. Also catches whitespace-only inputs the same way. New unit tests pin the contract. 2. (inline, Major) drift.cjs library purity — the earlier remediation passed `projectDir` into `detectDrift` and re-resolved runtime inside the library. CodeRabbit (correctly) flagged this as breaking the module's pure-library contract. detectDrift now accepts `input.runtime` directly; verify.cmdVerifyCodebaseDrift resolves the runtime once and passes the literal name in. drift.cjs no longer reads env or config at all. 3. (duplicate inline, Minor) gsd2-import.cjs:475 — when `gsd2-import --path <dir>` targets a project that isn't the process cwd, the preview command was formatted for the wrong runtime. buildPreview now receives the resolved `projectDir` (the same one used to find the .gsd/ root) instead of the raw `cwd`. 4. (outside-diff, Minor) tests/copilot-install.test.cjs:1-5 — the `allow-test-rule: integration-test-input` rationale block specifically named verify.cjs as the fixture, but #3584 changed that test to use a synthetic input. Comment now describes the real shape of the file's readFileSync usage (commands/, agents/, install.js source inputs to the installer/converter functions under test) and notes the synthetic substitution for the bin/lib path. Full suite: 9366/9366 pass (+1 new test). Lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.5 KiB
JavaScript
110 lines
4.5 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* runtime-slash.cjs — single source of truth for emitting GSD slash-command
|
|
* references in user-facing runtime output (recommended-actions JSON, persisted
|
|
* ROADMAP.md entries, verify/validate fix hints, error messages, etc.).
|
|
*
|
|
* Background: #2808 unified all GSD skill installs to register under the hyphen
|
|
* form (`name: gsd-<cmd>`). The legacy colon form `/gsd:<cmd>` is no longer
|
|
* routable by Claude Code skill installs, but ~50 runtime emissions in
|
|
* bin/lib/*.cjs still hardcoded it (#3584). Codex installs need the shell-var
|
|
* `$gsd-<cmd>` form. This module is the only place the runtime should decide
|
|
* which shape to emit.
|
|
*
|
|
* - codex: $gsd-<cmd> (shell-var syntax)
|
|
* - claude, cursor, opencode, kilo, etc.: /gsd-<cmd>
|
|
*
|
|
* The colon form is never emitted.
|
|
*/
|
|
|
|
function formatGsdSlash(commandName, runtime) {
|
|
if (typeof commandName !== 'string') return commandName;
|
|
if (commandName === '') return commandName;
|
|
|
|
// Strip any existing leading prefix so the helper is idempotent and accepts
|
|
// both legacy `/gsd:<name>` and canonical hyphen-form input (plus the bare
|
|
// `gsd:<name>` shorthand and codex `$gsd-<name>` shell-var input).
|
|
const stripped = commandName.replace(/^[/$]?gsd[-:]/i, '');
|
|
// If the regex matched nothing (no prefix), the input is already a bare name.
|
|
const bare = stripped === commandName ? commandName : stripped;
|
|
// Defensive: a degenerate input like `/gsd:`, `gsd-`, or whitespace-only
|
|
// normalizes to empty. Returning the original colon-form would re-emit the
|
|
// deprecated shape that this module exists to suppress (#3584). Return an
|
|
// empty string so callers see "no command" rather than the broken input.
|
|
if (bare === '' || bare.trim() === '') return '';
|
|
|
|
// Split on the first whitespace so only the command token is rewritten —
|
|
// anything after the first space is caller-supplied arguments (phase
|
|
// numbers, --flags, --paths C:\\Users\\Me, etc.) that must round-trip
|
|
// untouched. Codex lowercases only the command token; preserving the
|
|
// argument tail prevents path/flag corruption on case-sensitive systems.
|
|
const wsMatch = bare.match(/^(\S+)(\s[\s\S]*)?$/);
|
|
const token = wsMatch ? wsMatch[1] : bare;
|
|
const tail = wsMatch && wsMatch[2] ? wsMatch[2] : '';
|
|
|
|
const rt = String(runtime || 'claude').toLowerCase();
|
|
if (rt === 'codex') {
|
|
// Codex skills are invoked as $gsd-<cmd> (shell-var syntax). The command
|
|
// token is lowercased because shell-var identifiers are conventionally
|
|
// lowercase; matches the convertCodexSlash() projection in bin/install.js.
|
|
return `$gsd-${token.toLowerCase()}${tail}`;
|
|
}
|
|
return `/gsd-${token}${tail}`;
|
|
}
|
|
|
|
/**
|
|
* Resolve the effective runtime for a project directory.
|
|
*
|
|
* process.env.GSD_RUNTIME > config.runtime > 'claude'
|
|
*
|
|
* Mirrors the precedence already used by profile-output.cjs and the rest of
|
|
* the runtime resolution chain. Returns a lowercased string so downstream
|
|
* comparisons can be case-blind.
|
|
*
|
|
* @param {string|null|undefined} projectDir
|
|
* @returns {string}
|
|
*/
|
|
function resolveRuntime(projectDir) {
|
|
if (process.env.GSD_RUNTIME) {
|
|
return String(process.env.GSD_RUNTIME).toLowerCase();
|
|
}
|
|
if (projectDir) {
|
|
try {
|
|
// Read config.json directly (not via loadConfig). loadConfig has a side
|
|
// effect of normalizing and re-writing legacy keys back to disk, which
|
|
// would mutate the project file just to read the runtime name. We only
|
|
// need the literal `runtime:` value, so a plain JSON read is sufficient
|
|
// and side-effect-free.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const configPath = path.join(projectDir, '.planning', 'config.json');
|
|
if (fs.existsSync(configPath)) {
|
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed && typeof parsed === 'object' && parsed.runtime) {
|
|
return String(parsed.runtime).toLowerCase();
|
|
}
|
|
}
|
|
} catch {
|
|
// Fall through to default — a missing/broken config must not crash
|
|
// runtime output formatting.
|
|
}
|
|
}
|
|
return 'claude';
|
|
}
|
|
|
|
/**
|
|
* Convenience: format using the runtime resolved from a project directory.
|
|
* Equivalent to `formatGsdSlash(name, resolveRuntime(projectDir))`.
|
|
*/
|
|
function formatGsdSlashFor(projectDir, commandName) {
|
|
return formatGsdSlash(commandName, resolveRuntime(projectDir));
|
|
}
|
|
|
|
module.exports = {
|
|
formatGsdSlash,
|
|
resolveRuntime,
|
|
formatGsdSlashFor,
|
|
};
|