* feat: add requireFreshDist helper for gen-*.mjs scripts Adds sdk/scripts/_gen-helpers.mjs exporting requireFreshDist(distPath, tsSourcePath). Performs a synchronous mtime check before any generator reads sdk/dist/ — if the dist file is missing or older than its TS source, exits 1 with a clear actionable error naming both paths, both mtimes, and the npm run build:sdk hint. Single source of truth so all 9 generators import one function rather than duplicating the logic. See issue #168. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: enforce dist freshness in all 9 gen-*.mjs generators Each generator now calls requireFreshDist() before reading sdk/dist/. If the dist artifact is missing or stale relative to its TS source, the generator exits 1 with a clear error instead of silently emitting stale CJS output. Addresses the PR #154 incident where an agent edited a TS source, regenerated the CJS without rebuilding, and silently overwrote a fix. gen-configuration.mjs also had its existing throw-on-missing guard replaced with requireFreshDist() which additionally catches staleness. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add staleness-check regression for gen-*.mjs tests/gen-staleness-check.test.cjs covers three cases for all 9 generators: - exits 1 with "does not exist" + build hint when dist file is absent - exits 1 with stale-dist error (paths + mtimes) when TS source is newer than dist - exits 0 when dist is newer than TS source (skipped if sdk/dist not built) Uses child_process.spawnSync to exercise the real gen-*.mjs entry path. 27/27 passing locally with sdk/dist present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(gen-staleness-check): isolate dist-mutation tests with GSD_REPO_ROOT temp dirs Subtests A and B in gen-staleness-check.test.cjs were renaming/creating real sdk/dist files in the live repo tree while the suite ran at --test-concurrency=4. Other parallel test processes (e.g. frontmatter-cli.test.cjs) spawned subprocesses that attempted dynamic ESM import of sdk/dist/query/schema-detect.js while it was temporarily absent, producing unhandled ENOENT failures unrelated to the staleness guard logic under test. Fix: add a GSD_REPO_ROOT env override to _gen-helpers.mjs so requireFreshDist() resolves dist/ts paths against the provided root instead of the repo root derived from import.meta.url. The test creates an isolated tmpdir tree per subtest (with the real TS source copied in) and passes GSD_REPO_ROOT=<tmpdir> to the generator subprocess, so no real dist files are ever touched during subtests A or B. Subtest C (exits 0 on fresh dist) still uses the real repo tree because it needs actual compiled output to exercise the generator end-to-end, but only sets the TS source mtime (safe under concurrency) — it does not remove or rename any dist file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.4 KiB
JavaScript
100 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generator for the Project-Root Resolution Module CJS artifact.
|
|
*
|
|
* Imports the compiled ESM output from sdk/dist/project-root/index.js,
|
|
* captures findProjectRoot via Function.prototype.toString(), then emits
|
|
* get-shit-done/bin/lib/project-root.generated.cjs.
|
|
*
|
|
* Run: cd sdk && npm run gen:project-root
|
|
* Freshness check: node sdk/scripts/check-project-root-fresh.mjs
|
|
*/
|
|
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { requireFreshDist } from './_gen-helpers.mjs';
|
|
|
|
requireFreshDist('sdk/dist/project-root/index.js', 'sdk/src/project-root/index.ts');
|
|
|
|
const BANNER = `'use strict';
|
|
|
|
/**
|
|
* GENERATED FILE — DO NOT EDIT.
|
|
*
|
|
* Source: sdk/src/project-root/index.ts
|
|
* Regenerate: cd sdk && npm run gen:project-root
|
|
*
|
|
* Project-Root Resolution Module — resolves a project root from a starting
|
|
* directory by walking the ancestor chain and applying four heuristics:
|
|
* (0) own .planning/ guard (#1362)
|
|
* (1) parent .planning/config.json sub_repos
|
|
* (2) legacy multiRepo: true + ancestor .git
|
|
* (3) .git heuristic with parent .planning/
|
|
* Bounded by FIND_PROJECT_ROOT_MAX_DEPTH ancestors. Sync I/O.
|
|
*/
|
|
|
|
`;
|
|
|
|
/**
|
|
* Build the CJS content string. Exported so the freshness-check script can
|
|
* import this function directly (Phase 3's cleaner pattern) instead of
|
|
* duplicating the logic.
|
|
*/
|
|
export async function buildProjectRootCjs() {
|
|
const distUrl = new URL('../dist/project-root/index.js', import.meta.url);
|
|
const { findProjectRoot, FIND_PROJECT_ROOT_MAX_DEPTH } = await import(distUrl.href);
|
|
|
|
const findProjectRootBody = findProjectRoot.toString();
|
|
|
|
// The compiled ESM uses destructured named imports:
|
|
// import { dirname, resolve, sep, relative, parse as parsePath } from 'node:path';
|
|
// import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
// import { homedir } from 'node:os';
|
|
//
|
|
// In CJS we provide these as module-level constants so the function body
|
|
// can reference them as closed-over variables (same technique used in
|
|
// Phase 3 gen-workstream-inventory-builder.mjs for relative/sep/etc.).
|
|
const preamble = [
|
|
`const fs = require('fs');`,
|
|
`const path = require('path');`,
|
|
`const os = require('os');`,
|
|
`const { existsSync, readFileSync, statSync } = fs;`,
|
|
`const { dirname, resolve, sep, relative, parse: parsePath } = path;`,
|
|
`const { homedir } = os;`,
|
|
`const FIND_PROJECT_ROOT_MAX_DEPTH = ${FIND_PROJECT_ROOT_MAX_DEPTH};`,
|
|
].join('\n');
|
|
|
|
const parts = [
|
|
BANNER.trimEnd(),
|
|
'',
|
|
preamble,
|
|
'',
|
|
findProjectRootBody,
|
|
'',
|
|
`module.exports = { findProjectRoot };`,
|
|
'',
|
|
];
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
const content = await buildProjectRootCjs();
|
|
const outPath = fileURLToPath(
|
|
new URL('../../get-shit-done/bin/lib/project-root.generated.cjs', import.meta.url),
|
|
);
|
|
await writeFile(outPath, content, 'utf-8');
|
|
console.log(`Written: ${outPath}`);
|
|
}
|
|
|
|
// Only run main() when this file is the entry point, not when imported.
|
|
// process.argv[1] is already an absolute filesystem path on every platform Node
|
|
// supports; comparing directly avoids the Windows URL-parsing bug where
|
|
// `C:\\…\\gen-*.mjs` is misread as scheme "c:" by `new URL(...)`.
|
|
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|