Adds get-shit-done/bin/lib/package-identity.cjs as the single source of truth for PACKAGE_NAME, derived from package.json `name` via require. Refactors all runtime code-line occurrences in bin/install.js, get-shit-done/bin/check-latest-version.cjs, get-shit-done/bin/lib/shell-command-projection.cjs, get-shit-done/bin/lib/verify.cjs, scripts/changeset/cli.cjs, scripts/changeset/github-release-notes.cjs, and scripts/release-tarball-smoke.cjs to import PACKAGE_NAME from the identity module instead of hardcoding the literal. The package name is unchanged (@opengsd/get-shit-done-redux). Behaviour is byte-identical: all --help, hint, and release-notes strings render exactly as before. Golden-literal tests (bug-2992, bug-378) keep their hardcoded expected values and remain GREEN. Adds tests/package-name-single-source.test.cjs lint guard: fails CI if @opengsd/get-shit-done-redux appears as a code-line literal in runtime .cjs/.js outside the identity module, enforcing a one-file rename path. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -21,10 +21,11 @@
|
||||
*/
|
||||
|
||||
const { execNpm } = require('./lib/shell-command-projection.cjs');
|
||||
const { PACKAGE_NAME } = require('./lib/package-identity.cjs');
|
||||
|
||||
// Hardcoded. Do not parameterise — the whole point of this script is that
|
||||
// the package name is not a runtime choice for the caller.
|
||||
const PACKAGE_NAME = '@opengsd/get-shit-done-redux';
|
||||
// Sourced from package.json via package-identity.cjs (#516). Do not
|
||||
// parameterise — the whole point of this script is that the package name is
|
||||
// not a runtime choice for the caller.
|
||||
|
||||
const CHECK_REASON = Object.freeze({
|
||||
OK: 'ok',
|
||||
|
||||
19
get-shit-done/bin/lib/package-identity.cjs
Normal file
19
get-shit-done/bin/lib/package-identity.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Single source of truth for the package name at runtime (#516).
|
||||
*
|
||||
* Why this exists: the package name `@opengsd/get-shit-done-redux` was
|
||||
* hardcoded as a string literal in ~15 runtime .cjs/.js files. When the
|
||||
* package is renamed (e.g. to `@opengsd/gsd-core`), changing this one
|
||||
* require path propagates the new name everywhere in runtime code.
|
||||
*
|
||||
* Path: get-shit-done/bin/lib/package-identity.cjs
|
||||
* Resolves package.json both in-repo (3 levels up) and when shipped
|
||||
* (package root is always 3 dirs above this file).
|
||||
*/
|
||||
const pkg = require('../../../package.json');
|
||||
|
||||
module.exports = {
|
||||
PACKAGE_NAME: pkg.name,
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { PACKAGE_NAME } = require('./package-identity.cjs');
|
||||
// Use non-destructured access so test-time mock.method(childProcess, 'spawnSync')
|
||||
// can intercept calls from this seam — destructured imports capture references
|
||||
// at load time and become un-mockable.
|
||||
@@ -350,12 +351,12 @@ function formatSdkPathDiagnostic({ shimDir, platform, runDir }) {
|
||||
} else {
|
||||
actionLines.push('Could not locate a writable PATH directory to install the shim.');
|
||||
actionLines.push('Install globally to materialize the bin symlink:');
|
||||
actionLines.push('npm install -g @opengsd/get-shit-done-redux');
|
||||
actionLines.push(`npm install -g ${PACKAGE_NAME}`);
|
||||
}
|
||||
const npxNoteLines = isNpx
|
||||
? [
|
||||
"Note: you're running via npx. For a persistent shim,",
|
||||
'install globally instead: npm install -g @opengsd/get-shit-done-redux',
|
||||
`install globally instead: npm install -g ${PACKAGE_NAME}`,
|
||||
]
|
||||
: [];
|
||||
return { shimLocationLine, actionLines, shellActions, npxNoteLines, isNpx, isWin32 };
|
||||
|
||||
@@ -14,6 +14,7 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
const { loadConfig, normalizePhaseName, escapeRegex, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error, checkAgentsInstalled, CONFIG_DEFAULTS, inspectWorktreeHealth } = require('./core.cjs');
|
||||
const { execGit, platformReadSync: safeReadFile, platformWriteSync } = require('./shell-command-projection.cjs');
|
||||
const { PACKAGE_NAME } = require('./package-identity.cjs');
|
||||
const { planningDir } = require('./planning-workspace.cjs');
|
||||
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
||||
const { writeStateMd } = require('./state.cjs');
|
||||
@@ -867,11 +868,11 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
if (agentStatus.installed_agents.length === 0) {
|
||||
addIssue('warning', 'W010',
|
||||
`No GSD agents found in ${agentStatus.agents_dir} — Task(subagent_type="gsd-*") will fall back to general-purpose`,
|
||||
'Run the GSD installer: npx @opengsd/get-shit-done-redux@latest');
|
||||
`Run the GSD installer: npx ${PACKAGE_NAME}@latest`);
|
||||
} else {
|
||||
addIssue('warning', 'W010',
|
||||
`Missing ${agentStatus.missing_agents.length} GSD agents: ${agentStatus.missing_agents.join(', ')} — affected workflows will fall back to general-purpose`,
|
||||
'Run the GSD installer: npx @opengsd/get-shit-done-redux@latest');
|
||||
`Run the GSD installer: npx ${PACKAGE_NAME}@latest`);
|
||||
}
|
||||
}
|
||||
} catch { /* intentionally empty — agent check is non-blocking */ }
|
||||
|
||||
@@ -25,6 +25,7 @@ const {
|
||||
compareSemverCore,
|
||||
isStableTripletSemver,
|
||||
} = require('../../get-shit-done/bin/lib/semver-compare.cjs');
|
||||
const { PACKAGE_NAME } = require('../../get-shit-done/bin/lib/package-identity.cjs');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const opts = {
|
||||
@@ -37,7 +38,7 @@ function parseArgs(argv) {
|
||||
changelog: null,
|
||||
output: null,
|
||||
repoSlug: 'open-gsd/get-shit-done-redux',
|
||||
installCommand: 'npx @opengsd/get-shit-done-redux@latest',
|
||||
installCommand: `npx ${PACKAGE_NAME}@latest`,
|
||||
json: false,
|
||||
};
|
||||
if (argv.length === 0) return { ok: true, opts };
|
||||
|
||||
@@ -4,6 +4,7 @@ const cp = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
|
||||
const { parseFragment } = require('./parse.cjs');
|
||||
const { PACKAGE_NAME } = require('../../get-shit-done/bin/lib/package-identity.cjs');
|
||||
|
||||
const SECTION_ORDER = ['Fixed', 'Added', 'Changed', 'Deprecated', 'Removed', 'Security'];
|
||||
|
||||
@@ -146,7 +147,7 @@ function serializeGithubReleaseNotes({
|
||||
fromRef,
|
||||
toRef,
|
||||
repoSlug = 'open-gsd/get-shit-done-redux',
|
||||
installCommand = 'npx @opengsd/get-shit-done-redux@latest',
|
||||
installCommand = `npx ${PACKAGE_NAME}@latest`,
|
||||
}) {
|
||||
if (installCommand.includes('`')) {
|
||||
throw new Error('installCommand cannot contain backtick characters');
|
||||
|
||||
@@ -45,6 +45,7 @@ const { execFileSync, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { PACKAGE_NAME } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
||||
// 120 s proved too tight on Windows GitHub-hosted runners: cold-cache
|
||||
// `npm install -g` with a 1499-file tarball took ~120 s exactly, causing
|
||||
// spawnSync to fire SIGTERM and return { status: null, stdout: '', stderr: '' }
|
||||
@@ -121,10 +122,12 @@ function binInvocation(binPath, args = []) {
|
||||
* an npm --prefix install directory.
|
||||
*/
|
||||
function pkgRoot(installPrefix) {
|
||||
// POSIX: <prefix>/lib/node_modules/@opengsd/get-shit-done-redux
|
||||
// Windows: <prefix>/node_modules/@opengsd/get-shit-done-redux
|
||||
const posix = path.join(installPrefix, 'lib', 'node_modules', '@opengsd', 'get-shit-done-redux');
|
||||
const win = path.join(installPrefix, 'node_modules', '@opengsd', 'get-shit-done-redux');
|
||||
// POSIX: <prefix>/lib/node_modules/<scope>/<pkg>
|
||||
// Windows: <prefix>/node_modules/<scope>/<pkg>
|
||||
// PACKAGE_NAME is scoped (@scope/pkg), so split('/') yields the two path segments.
|
||||
const pkgSegments = PACKAGE_NAME.split('/');
|
||||
const posix = path.join(installPrefix, 'lib', 'node_modules', ...pkgSegments);
|
||||
const win = path.join(installPrefix, 'node_modules', ...pkgSegments);
|
||||
return fs.existsSync(posix) ? posix : win;
|
||||
}
|
||||
|
||||
|
||||
115
tests/package-name-single-source.test.cjs
Normal file
115
tests/package-name-single-source.test.cjs
Normal file
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Lint guard: the package name must be single-sourced from package-identity.cjs.
|
||||
*
|
||||
* Scans runtime files (bin/install.js, get-shit-done/bin/**, scripts/*.cjs)
|
||||
* and FAILS if the literal `@opengsd/get-shit-done-redux` appears in a
|
||||
* non-comment, non-identity-module line. This enforces that a future rename
|
||||
* is a one-file change in package.json (#516).
|
||||
*
|
||||
* Comment lines are detected by checking if the trimmed line starts with
|
||||
* `//`, `*`, or `/*` (best-effort; covers all common JS comment forms).
|
||||
*
|
||||
* Tests:
|
||||
* 1. No code-literal occurrences of the package name in runtime files
|
||||
* (outside the identity module itself).
|
||||
* 2. PACKAGE_NAME exported from the identity module equals package.json name.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const LITERAL = '@opengsd/get-shit-done-redux';
|
||||
const IDENTITY_MODULE = path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'package-identity.cjs');
|
||||
|
||||
// Files to scan: bin/install.js + everything under get-shit-done/bin/ + touched scripts
|
||||
function getRuntimeFiles() {
|
||||
const files = [];
|
||||
|
||||
// bin/install.js
|
||||
const installJs = path.join(ROOT, 'bin', 'install.js');
|
||||
if (fs.existsSync(installJs)) files.push(installJs);
|
||||
|
||||
// get-shit-done/bin/**/*.cjs and *.js (recursive)
|
||||
function collectDir(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
collectDir(fullPath);
|
||||
} else if (entry.isFile() && /\.(cjs|js)$/.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
collectDir(path.join(ROOT, 'get-shit-done', 'bin'));
|
||||
|
||||
// scripts/*.cjs
|
||||
for (const entry of fs.readdirSync(path.join(ROOT, 'scripts'), { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.endsWith('.cjs')) {
|
||||
files.push(path.join(ROOT, 'scripts', entry.name));
|
||||
}
|
||||
}
|
||||
// scripts/changeset/*.cjs
|
||||
const changesetDir = path.join(ROOT, 'scripts', 'changeset');
|
||||
if (fs.existsSync(changesetDir)) {
|
||||
for (const entry of fs.readdirSync(changesetDir, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.endsWith('.cjs')) {
|
||||
files.push(path.join(changesetDir, entry.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function isCommentLine(trimmed) {
|
||||
return (
|
||||
trimmed.startsWith('//') ||
|
||||
trimmed.startsWith('*') ||
|
||||
trimmed.startsWith('/*')
|
||||
);
|
||||
}
|
||||
|
||||
test('no hardcoded package-name literals in runtime non-comment code lines (#516)', () => {
|
||||
const files = getRuntimeFiles();
|
||||
const violations = [];
|
||||
|
||||
for (const file of files) {
|
||||
// Skip the identity module itself — it legitimately contains the literal
|
||||
if (path.resolve(file) === path.resolve(IDENTITY_MODULE)) continue;
|
||||
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line.includes(LITERAL)) continue;
|
||||
const trimmed = line.trimStart();
|
||||
if (isCommentLine(trimmed)) continue;
|
||||
violations.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim()}`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
violations,
|
||||
[],
|
||||
`Found ${violations.length} hardcoded package-name literal(s) in non-comment code lines:\n` +
|
||||
violations.map(v => ` ${v}`).join('\n') +
|
||||
'\n\nReplace each with the PACKAGE_NAME imported from get-shit-done/bin/lib/package-identity.cjs'
|
||||
);
|
||||
});
|
||||
|
||||
test('PACKAGE_NAME from identity module matches package.json name (#516)', () => {
|
||||
const { PACKAGE_NAME } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
||||
const pkgJson = require('../package.json');
|
||||
assert.equal(
|
||||
PACKAGE_NAME,
|
||||
pkgJson.name,
|
||||
`Identity module PACKAGE_NAME (${PACKAGE_NAME}) must equal package.json name (${pkgJson.name})`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user