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:
6
.changeset/3571-configuration-manifest-install-layout.md
Normal file
6
.changeset/3571-configuration-manifest-install-layout.md
Normal 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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {`,
|
||||
|
||||
124
tests/bug-3571-configuration-manifest-install-path.test.cjs
Normal file
124
tests/bug-3571-configuration-manifest-install-path.test.cjs
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user