fix(3571): load configuration manifests after install (#3572)

* fix(3571): load configuration manifests after install

* test(3571): simplify missing manifest check
This commit is contained in:
Maxim Brashenko
2026-05-16 07:03:03 +05:00
committed by GitHub
parent 6a2bf05de7
commit 79ea076daa
5 changed files with 206 additions and 27 deletions

View File

@@ -0,0 +1,6 @@
---
type: Fixed
pr: 3572
---
**Installed `gsd-tools.cjs` can load generated configuration manifests** — runtime installs now copy `config-defaults.manifest.json` and `config-schema.manifest.json` into `get-shit-done/bin/shared/`, and `configuration.generated.cjs` resolves that co-located install path before falling back to the source checkout `sdk/shared/` path. This prevents direct installed CJS invocations from failing with `MODULE_NOT_FOUND` when no sibling `sdk/` checkout exists. (#3571)

View File

@@ -8425,26 +8425,35 @@ function install(isGlobal, runtime = 'claude', options = {}) {
failures.push('get-shit-done');
}
// #3288 — Copy sdk/shared/model-catalog.json into the get-shit-done payload
// at the co-located path that model-catalog.cjs resolves first:
// get-shit-done/bin/shared/model-catalog.json
// #3288 / #3571 — Copy sdk/shared manifests into the get-shit-done payload
// at the co-located path that CJS modules resolve first:
// get-shit-done/bin/shared/*.json
//
// The install copies get-shit-done/ but NOT sdk/ — the CJS module's legacy
// path (3 levels up → sdk/shared/) therefore resolves to a non-existent
// location in every post-install layout. Copying the catalog alongside the
// CJS files ensures require() succeeds without needing sdk/ to exist.
const modelCatalogSrc = path.join(src, 'sdk', 'shared', 'model-catalog.json');
const modelCatalogDest = path.join(skillDest, 'bin', 'shared', 'model-catalog.json');
if (fs.existsSync(modelCatalogSrc)) {
fs.mkdirSync(path.dirname(modelCatalogDest), { recursive: true });
fs.copyFileSync(modelCatalogSrc, modelCatalogDest);
if (verifyFileInstalled(modelCatalogDest, 'get-shit-done/bin/shared/model-catalog.json')) {
console.log(` ${green}✓${reset} Installed get-shit-done/bin/shared/model-catalog.json`);
// The install copies get-shit-done/ but NOT sdk/ — CJS modules' legacy
// source-repo paths (3 levels up → sdk/shared/) therefore resolve to a
// non-existent location in every post-install layout. Copying these shared
// files alongside the CJS files ensures require() succeeds without needing
// sdk/ to exist.
const sharedPayloadFiles = [
'model-catalog.json',
'config-defaults.manifest.json',
'config-schema.manifest.json',
];
for (const fileName of sharedPayloadFiles) {
const sharedSrc = path.join(src, 'sdk', 'shared', fileName);
const sharedDest = path.join(skillDest, 'bin', 'shared', fileName);
const displayPath = `get-shit-done/bin/shared/${fileName}`;
if (fs.existsSync(sharedSrc)) {
fs.mkdirSync(path.dirname(sharedDest), { recursive: true });
fs.copyFileSync(sharedSrc, sharedDest);
if (verifyFileInstalled(sharedDest, displayPath)) {
console.log(` ${green}✓${reset} Installed ${displayPath}`);
} else {
failures.push(displayPath);
}
} else {
failures.push('get-shit-done/bin/shared/model-catalog.json');
failures.push(`sdk/shared/${fileName} (source missing)`);
}
} else {
failures.push('sdk/shared/model-catalog.json (source missing)');
}
// Copy agents to agents directory.

View File

@@ -14,11 +14,31 @@ const { readFileSync, writeFileSync, existsSync, readdirSync } = require('node:f
const { join } = require('node:path');
// ─── Manifest requires ───────────────────────────────────────────────────────
// Resolved relative to this file: get-shit-done/bin/lib/ → sdk/shared/
// This file lives at: get-shit-done/bin/lib/configuration.generated.cjs
// sdk/shared lives at: sdk/shared/ (3 dirs up from bin/lib, then into sdk/shared)
const CONFIG_DEFAULTS = require('../../../sdk/shared/config-defaults.manifest.json');
const SCHEMA_MANIFEST = require('../../../sdk/shared/config-schema.manifest.json');
function loadConfigurationManifest(fileName) {
const candidates = [
// Installed runtime layout: get-shit-done/bin/shared/*.manifest.json
join(__dirname, '..', 'shared', fileName),
// Source-repo dev layout: sdk/shared/*.manifest.json
join(__dirname, '..', '..', '..', 'sdk', 'shared', fileName),
];
let lastErr = null;
for (const candidate of candidates) {
try {
return require(candidate);
} catch (err) {
const isMissingCandidate =
err && err.code === 'MODULE_NOT_FOUND' && String(err.message || '').includes(candidate);
if (!isMissingCandidate) throw err;
lastErr = err;
}
}
throw new Error(
`${fileName} not found. Tried:\n${candidates.map((p) => ` ${p}`).join('\n')}\nLast error: ${lastErr?.message}`
);
}
const CONFIG_DEFAULTS = loadConfigurationManifest('config-defaults.manifest.json');
const SCHEMA_MANIFEST = loadConfigurationManifest('config-schema.manifest.json');
const VALID_CONFIG_KEYS = new Set(SCHEMA_MANIFEST.validKeys);
const RUNTIME_STATE_KEYS = new Set(SCHEMA_MANIFEST.runtimeStateKeys);
const DYNAMIC_KEY_PATTERNS = SCHEMA_MANIFEST.dynamicKeyPatterns.map((p) => {

View File

@@ -98,11 +98,31 @@ export function buildConfigurationCjs() {
`const { join } = require('node:path');`,
``,
`// ─── Manifest requires ───────────────────────────────────────────────────────`,
`// Resolved relative to this file: get-shit-done/bin/lib/ → sdk/shared/`,
`// This file lives at: get-shit-done/bin/lib/configuration.generated.cjs`,
`// sdk/shared lives at: sdk/shared/ (3 dirs up from bin/lib, then into sdk/shared)`,
`const CONFIG_DEFAULTS = require('../../../sdk/shared/config-defaults.manifest.json');`,
`const SCHEMA_MANIFEST = require('../../../sdk/shared/config-schema.manifest.json');`,
`function loadConfigurationManifest(fileName) {`,
` const candidates = [`,
` // Installed runtime layout: get-shit-done/bin/shared/*.manifest.json`,
` join(__dirname, '..', 'shared', fileName),`,
` // Source-repo dev layout: sdk/shared/*.manifest.json`,
` join(__dirname, '..', '..', '..', 'sdk', 'shared', fileName),`,
` ];`,
` let lastErr = null;`,
` for (const candidate of candidates) {`,
` try {`,
` return require(candidate);`,
` } catch (err) {`,
` const isMissingCandidate =`,
` err && err.code === 'MODULE_NOT_FOUND' && String(err.message || '').includes(candidate);`,
` if (!isMissingCandidate) throw err;`,
` lastErr = err;`,
` }`,
` }`,
` throw new Error(`,
` \`\${fileName} not found. Tried:\\n\${candidates.map((p) => \` \${p}\`).join('\\n')}\\nLast error: \${lastErr?.message}\``,
` );`,
`}`,
``,
`const CONFIG_DEFAULTS = loadConfigurationManifest('config-defaults.manifest.json');`,
`const SCHEMA_MANIFEST = loadConfigurationManifest('config-schema.manifest.json');`,
`const VALID_CONFIG_KEYS = new Set(SCHEMA_MANIFEST.validKeys);`,
`const RUNTIME_STATE_KEYS = new Set(SCHEMA_MANIFEST.runtimeStateKeys);`,
`const DYNAMIC_KEY_PATTERNS = SCHEMA_MANIFEST.dynamicKeyPatterns.map((p) => {`,

View File

@@ -0,0 +1,124 @@
/**
* Regression test for #3571: configuration.generated.cjs used the source
* checkout sdk/shared path only, which breaks installed gsd-tools.cjs because
* runtime installs copy get-shit-done/ but not sdk/.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const REPO_ROOT = path.join(__dirname, '..');
const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.generated.cjs');
const SDK_SHARED_DIR = path.join(REPO_ROOT, 'sdk', 'shared');
const { install } = require('../bin/install.js');
function makeTmpDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3571-'));
}
function silenceConsole(fn) {
const original = {
log: console.log,
warn: console.warn,
error: console.error,
};
console.log = () => {};
console.warn = () => {};
console.error = () => {};
try {
return fn();
} finally {
console.log = original.log;
console.warn = original.warn;
console.error = original.error;
}
}
describe('bug #3571: configuration generated manifests resolve in install layout', () => {
let tmpRoot;
let savedHome;
let savedExplicitConfigDir;
beforeEach(() => {
tmpRoot = makeTmpDir();
savedHome = process.env.HOME;
savedExplicitConfigDir = process.env.GSD_EXPLICIT_CONFIG_DIR;
delete process.env.GSD_EXPLICIT_CONFIG_DIR;
});
afterEach(() => {
process.env.HOME = savedHome;
if (savedExplicitConfigDir === undefined) {
delete process.env.GSD_EXPLICIT_CONFIG_DIR;
} else {
process.env.GSD_EXPLICIT_CONFIG_DIR = savedExplicitConfigDir;
}
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
test('co-located bin/shared manifests let configuration.generated.cjs load without sdk/shared', () => {
const gsdBinDir = path.join(tmpRoot, '.codex', 'get-shit-done', 'bin');
const gsdLibDir = path.join(gsdBinDir, 'lib');
const gsdSharedDir = path.join(gsdBinDir, 'shared');
fs.mkdirSync(gsdLibDir, { recursive: true });
fs.mkdirSync(gsdSharedDir, { recursive: true });
const installedCjs = path.join(gsdLibDir, 'configuration.generated.cjs');
fs.copyFileSync(CONFIGURATION_CJS, installedCjs);
fs.copyFileSync(
path.join(SDK_SHARED_DIR, 'config-defaults.manifest.json'),
path.join(gsdSharedDir, 'config-defaults.manifest.json')
);
fs.copyFileSync(
path.join(SDK_SHARED_DIR, 'config-schema.manifest.json'),
path.join(gsdSharedDir, 'config-schema.manifest.json')
);
delete require.cache[installedCjs];
let mod;
assert.doesNotThrow(() => {
mod = require(installedCjs);
}, 'installed configuration.generated.cjs must not require ~/.codex/sdk/shared');
assert.ok(mod.VALID_CONFIG_KEYS.has('workflow.plan_review_convergence'));
});
test('post-install: install() copies configuration manifests to co-located bin/shared', () => {
process.env.HOME = tmpRoot;
silenceConsole(() => {
install(true, 'codex');
});
const sharedDir = path.join(tmpRoot, '.codex', 'get-shit-done', 'bin', 'shared');
for (const fileName of ['config-defaults.manifest.json', 'config-schema.manifest.json']) {
const installedManifest = path.join(sharedDir, fileName);
assert.ok(fs.existsSync(installedManifest), `${fileName} must be copied to ${sharedDir}`);
assert.doesNotThrow(() => {
JSON.parse(fs.readFileSync(installedManifest, 'utf8'));
}, `${fileName} must be valid JSON`);
}
const installedCjs = path.join(
tmpRoot,
'.codex',
'get-shit-done',
'bin',
'lib',
'configuration.generated.cjs'
);
delete require.cache[installedCjs];
assert.doesNotThrow(() => {
require(installedCjs);
}, 'post-install configuration.generated.cjs must load from co-located manifests');
});
});