From b54026e1067910abfcedb5802ecbab620e542f5c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 30 May 2026 13:07:58 -0400 Subject: [PATCH] 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 --- bin/install.js | 8 +- get-shit-done/bin/check-latest-version.cjs | 7 +- get-shit-done/bin/lib/package-identity.cjs | 19 +++ .../bin/lib/shell-command-projection.cjs | 5 +- get-shit-done/bin/lib/verify.cjs | 5 +- scripts/changeset/cli.cjs | 3 +- scripts/changeset/github-release-notes.cjs | 3 +- scripts/release-tarball-smoke.cjs | 11 +- tests/package-name-single-source.test.cjs | 115 ++++++++++++++++++ 9 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 get-shit-done/bin/lib/package-identity.cjs create mode 100644 tests/package-name-single-source.test.cjs diff --git a/bin/install.js b/bin/install.js index 2eecb8e6f..d6d076416 100755 --- a/bin/install.js +++ b/bin/install.js @@ -326,7 +326,7 @@ Please install a Linux-native Node.js inside WSL: curl -fsSL https://fnm.vercel.app/install | bash fnm install --lts -Then re-run: npx @opengsd/get-shit-done-redux@latest +Then re-run: npx ${pkg.name}@latest `); process.exit(1); } @@ -674,7 +674,7 @@ if (hasUninstall) { // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx @opengsd/get-shit-done-redux [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx @opengsd/get-shit-done-redux\n\n ${dim}# Install for Claude Code globally${reset}\n npx @opengsd/get-shit-done-redux --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx @opengsd/get-shit-done-redux --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx @opengsd/get-shit-done-redux --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx @opengsd/get-shit-done-redux --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx @opengsd/get-shit-done-redux --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx @opengsd/get-shit-done-redux --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx @opengsd/get-shit-done-redux --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx @opengsd/get-shit-done-redux --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx @opengsd/get-shit-done-redux --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx @opengsd/get-shit-done-redux --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx @opengsd/get-shit-done-redux --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx @opengsd/get-shit-done-redux --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx @opengsd/get-shit-done-redux --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx @opengsd/get-shit-done-redux --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx @opengsd/get-shit-done-redux --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx @opengsd/get-shit-done-redux --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx @opengsd/get-shit-done-redux --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx @opengsd/get-shit-done-redux --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx @opengsd/get-shit-done-redux --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx @opengsd/get-shit-done-redux --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx @opengsd/get-shit-done-redux --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx @opengsd/get-shit-done-redux --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx @opengsd/get-shit-done-redux --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx @opengsd/get-shit-done-redux --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx @opengsd/get-shit-done-redux --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`); + console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=${reset} Install a named skill profile. Profiles:\n core — 7 main-loop skills incl. phase (~130 desc tokens)\n standard — ~13 skills incl. phase, review, config (~700)\n full — all 66 skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`gsd update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n`); process.exit(0); } @@ -10754,13 +10754,13 @@ function formatStaleStandaloneSdkWarning(info) { const version = info && info.version ? info.version : '(unknown)'; return [ '⚠ A stale globally-installed @opengsd/gsd-sdk@' + version + ' is shadowing the', - ' `gsd-sdk` shim that @opengsd/get-shit-done-redux provides. The standalone package', + ' `gsd-sdk` shim that ' + pkg.name + ' provides. The standalone package', ' only knows `run | auto | init` — every `gsd-sdk query ` call from', ' skills and hooks will fail until you remove it.', '', ' Remediation:', ' npm uninstall -g @opengsd/gsd-sdk', - ' npx -y @opengsd/get-shit-done-redux@latest -- --global', + ' npx -y ' + pkg.name + '@latest -- --global', '', ' Tracking: #3406 — https://github.com/open-gsd/get-shit-done-redux/issues/3406', ].join('\n'); diff --git a/get-shit-done/bin/check-latest-version.cjs b/get-shit-done/bin/check-latest-version.cjs index aff7b03a4..f1e7391e5 100755 --- a/get-shit-done/bin/check-latest-version.cjs +++ b/get-shit-done/bin/check-latest-version.cjs @@ -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', diff --git a/get-shit-done/bin/lib/package-identity.cjs b/get-shit-done/bin/lib/package-identity.cjs new file mode 100644 index 000000000..8032b7b87 --- /dev/null +++ b/get-shit-done/bin/lib/package-identity.cjs @@ -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, +}; diff --git a/get-shit-done/bin/lib/shell-command-projection.cjs b/get-shit-done/bin/lib/shell-command-projection.cjs index 793d985c5..38ac0a87c 100644 --- a/get-shit-done/bin/lib/shell-command-projection.cjs +++ b/get-shit-done/bin/lib/shell-command-projection.cjs @@ -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 }; diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 53ae80a43..b54e17725 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -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 */ } diff --git a/scripts/changeset/cli.cjs b/scripts/changeset/cli.cjs index b208a54ec..d1d34d455 100755 --- a/scripts/changeset/cli.cjs +++ b/scripts/changeset/cli.cjs @@ -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 }; diff --git a/scripts/changeset/github-release-notes.cjs b/scripts/changeset/github-release-notes.cjs index 53988bd90..f08ded6ef 100644 --- a/scripts/changeset/github-release-notes.cjs +++ b/scripts/changeset/github-release-notes.cjs @@ -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'); diff --git a/scripts/release-tarball-smoke.cjs b/scripts/release-tarball-smoke.cjs index c65257741..d5fb16aff 100644 --- a/scripts/release-tarball-smoke.cjs +++ b/scripts/release-tarball-smoke.cjs @@ -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: /lib/node_modules/@opengsd/get-shit-done-redux - // Windows: /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: /lib/node_modules// + // Windows: /node_modules// + // 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; } diff --git a/tests/package-name-single-source.test.cjs b/tests/package-name-single-source.test.cjs new file mode 100644 index 000000000..34cc59d16 --- /dev/null +++ b/tests/package-name-single-source.test.cjs @@ -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})` + ); +});