chore(#516): single-source the package name from package.json (#517)

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:
Tom Boucher
2026-05-30 13:07:58 -04:00
committed by GitHub
parent a1f4996d9a
commit b54026e106
9 changed files with 159 additions and 17 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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',

View 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,
};

View File

@@ -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 };

View File

@@ -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 */ }

View File

@@ -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 };

View File

@@ -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');

View File

@@ -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;
}

View 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})`
);
});