frontmcp 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/commands/build/exec/bin-meta.d.ts +49 -0
- package/src/commands/build/exec/bin-meta.js +68 -0
- package/src/commands/build/exec/bin-meta.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
- package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
- package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
- package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
- package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
- package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
- package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
- package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
- package/src/commands/build/exec/index.js +26 -0
- package/src/commands/build/exec/index.js.map +1 -1
- package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
- package/src/commands/dev/bridge/child-supervisor.js +228 -0
- package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
- package/src/commands/dev/bridge/errors.d.ts +23 -0
- package/src/commands/dev/bridge/errors.js +34 -0
- package/src/commands/dev/bridge/errors.js.map +1 -0
- package/src/commands/dev/bridge/index.d.ts +30 -0
- package/src/commands/dev/bridge/index.js +220 -0
- package/src/commands/dev/bridge/index.js.map +1 -0
- package/src/commands/dev/bridge/log.d.ts +29 -0
- package/src/commands/dev/bridge/log.js +82 -0
- package/src/commands/dev/bridge/log.js.map +1 -0
- package/src/commands/dev/bridge/state-machine.d.ts +56 -0
- package/src/commands/dev/bridge/state-machine.js +245 -0
- package/src/commands/dev/bridge/state-machine.js.map +1 -0
- package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
- package/src/commands/dev/bridge/stdio-framer.js +128 -0
- package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
- package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
- package/src/commands/dev/bridge/upstream-client.js +159 -0
- package/src/commands/dev/bridge/upstream-client.js.map +1 -0
- package/src/commands/dev/bridge/watcher.d.ts +30 -0
- package/src/commands/dev/bridge/watcher.js +87 -0
- package/src/commands/dev/bridge/watcher.js.map +1 -0
- package/src/commands/dev/dev.d.ts +18 -1
- package/src/commands/dev/dev.js +134 -14
- package/src/commands/dev/dev.js.map +1 -1
- package/src/commands/dev/inspector.d.ts +13 -1
- package/src/commands/dev/inspector.js +77 -3
- package/src/commands/dev/inspector.js.map +1 -1
- package/src/commands/dev/port.d.ts +23 -0
- package/src/commands/dev/port.js +87 -0
- package/src/commands/dev/port.js.map +1 -0
- package/src/commands/dev/register.d.ts +1 -1
- package/src/commands/dev/register.js +28 -4
- package/src/commands/dev/register.js.map +1 -1
- package/src/commands/dev/test.d.ts +26 -1
- package/src/commands/dev/test.js +181 -64
- package/src/commands/dev/test.js.map +1 -1
- package/src/commands/eject/mcp-client.d.ts +25 -0
- package/src/commands/eject/mcp-client.js +74 -0
- package/src/commands/eject/mcp-client.js.map +1 -0
- package/src/commands/eject/register.d.ts +9 -0
- package/src/commands/eject/register.js +56 -0
- package/src/commands/eject/register.js.map +1 -0
- package/src/commands/install/install-claude-plugin.d.ts +13 -0
- package/src/commands/install/install-claude-plugin.js +327 -0
- package/src/commands/install/install-claude-plugin.js.map +1 -0
- package/src/commands/install/register.d.ts +16 -0
- package/src/commands/install/register.js +70 -0
- package/src/commands/install/register.js.map +1 -0
- package/src/commands/scaffold/create.js +44 -0
- package/src/commands/scaffold/create.js.map +1 -1
- package/src/commands/skills/from-entry.d.ts +31 -0
- package/src/commands/skills/from-entry.js +68 -0
- package/src/commands/skills/from-entry.js.map +1 -0
- package/src/commands/skills/install.d.ts +12 -0
- package/src/commands/skills/install.js +173 -8
- package/src/commands/skills/install.js.map +1 -1
- package/src/commands/skills/register.js +7 -3
- package/src/commands/skills/register.js.map +1 -1
- package/src/config/frontmcp-config.loader.d.ts +28 -0
- package/src/config/frontmcp-config.loader.js +146 -67
- package/src/config/frontmcp-config.loader.js.map +1 -1
- package/src/config/frontmcp-config.resolve.d.ts +67 -0
- package/src/config/frontmcp-config.resolve.js +118 -0
- package/src/config/frontmcp-config.resolve.js.map +1 -0
- package/src/config/frontmcp-config.schema.d.ts +207 -0
- package/src/config/frontmcp-config.schema.js +217 -1
- package/src/config/frontmcp-config.schema.js.map +1 -1
- package/src/config/frontmcp-config.types.d.ts +133 -0
- package/src/config/frontmcp-config.types.js.map +1 -1
- package/src/config/index.d.ts +2 -1
- package/src/config/index.js +3 -1
- package/src/config/index.js.map +1 -1
- package/src/core/args.d.ts +13 -0
- package/src/core/args.js.map +1 -1
- package/src/core/bridge.js +39 -0
- package/src/core/bridge.js.map +1 -1
- package/src/core/cli.d.ts +0 -6
- package/src/core/cli.js +23 -3
- package/src/core/cli.js.map +1 -1
- package/src/core/help.d.ts +1 -1
- package/src/core/help.js +27 -6
- package/src/core/help.js.map +1 -1
- package/src/core/program.d.ts +1 -1
- package/src/core/program.js +56 -12
- package/src/core/program.js.map +1 -1
- package/src/core/project-commands.d.ts +44 -0
- package/src/core/project-commands.js +216 -0
- package/src/core/project-commands.js.map +1 -0
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.loadFrontMcpConfig = loadFrontMcpConfig;
|
|
10
|
+
exports.loadFrontMcpConfigFromFile = loadFrontMcpConfigFromFile;
|
|
11
|
+
exports.findConfigDir = findConfigDir;
|
|
10
12
|
exports.tryLoadFrontMcpConfig = tryLoadFrontMcpConfig;
|
|
13
|
+
exports.tryLoadFrontMcpConfigFromFile = tryLoadFrontMcpConfigFromFile;
|
|
11
14
|
exports.validateConfig = validateConfig;
|
|
12
15
|
exports.findDeployment = findDeployment;
|
|
13
16
|
exports.getDeploymentTargets = getDeploymentTargets;
|
|
14
|
-
const tslib_1 = require("tslib");
|
|
15
|
-
const fs = tslib_1.__importStar(require("fs"));
|
|
16
|
-
const path = tslib_1.__importStar(require("path"));
|
|
17
17
|
const utils_1 = require("@frontmcp/utils");
|
|
18
18
|
const frontmcp_config_schema_1 = require("./frontmcp-config.schema");
|
|
19
19
|
const CONFIG_FILENAMES = [
|
|
@@ -38,6 +38,48 @@ async function loadFrontMcpConfig(cwd) {
|
|
|
38
38
|
const raw = await loadRawConfig(cwd);
|
|
39
39
|
return validateConfig(raw);
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Load a specific config file by absolute or cwd-relative path. Used by the
|
|
43
|
+
* `--config <path>` flag and the `FRONTMCP_CONFIG` env var (issue #400).
|
|
44
|
+
*
|
|
45
|
+
* Unlike `loadFrontMcpConfig`, this doesn't search `CONFIG_FILENAMES` — the
|
|
46
|
+
* caller already named the file, so a missing-file error is a hard failure
|
|
47
|
+
* (no silent fallback to `deriveFromPackageJson`).
|
|
48
|
+
*/
|
|
49
|
+
async function loadFrontMcpConfigFromFile(configPath) {
|
|
50
|
+
const absolutePath = (0, utils_1.isAbsolute)(configPath) ? configPath : (0, utils_1.pathResolve)(process.cwd(), configPath);
|
|
51
|
+
if (!(await (0, utils_1.fileExists)(absolutePath))) {
|
|
52
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
53
|
+
}
|
|
54
|
+
const filename = (0, utils_1.basename)(absolutePath);
|
|
55
|
+
const raw = await loadRawFileAtPath(absolutePath, filename);
|
|
56
|
+
return validateConfig(raw);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Locate the nearest `frontmcp.config.*` file by walking upward from `cwd`.
|
|
60
|
+
*
|
|
61
|
+
* Issue #400 — monorepo nested apps no longer require `cd <repo-root>`
|
|
62
|
+
* before invoking the CLI. The walk caps at 10 levels to avoid pathological
|
|
63
|
+
* symlink loops.
|
|
64
|
+
*
|
|
65
|
+
* Returns the directory containing the config (so callers can pass it to
|
|
66
|
+
* `loadFrontMcpConfig(dir)`), or `undefined` if nothing was found.
|
|
67
|
+
*/
|
|
68
|
+
async function findConfigDir(startDir, maxLevels = 10) {
|
|
69
|
+
let current = (0, utils_1.pathResolve)(startDir);
|
|
70
|
+
for (let i = 0; i <= maxLevels; i++) {
|
|
71
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
72
|
+
if (await (0, utils_1.fileExists)((0, utils_1.pathJoin)(current, filename))) {
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const parent = (0, utils_1.dirname)(current);
|
|
77
|
+
if (parent === current)
|
|
78
|
+
return undefined;
|
|
79
|
+
current = parent;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
41
83
|
/**
|
|
42
84
|
* Variant that load-errors propagate (parse failures in `frontmcp.config.ts`,
|
|
43
85
|
* missing dependencies, etc.) but schema-validation errors return `undefined`.
|
|
@@ -70,6 +112,32 @@ async function tryLoadFrontMcpConfig(cwd) {
|
|
|
70
112
|
}
|
|
71
113
|
throw err;
|
|
72
114
|
}
|
|
115
|
+
return parseRawOrLegacy(raw);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Explicit-path counterpart of {@link tryLoadFrontMcpConfig}. Used by
|
|
119
|
+
* `resolveConfig` for the `--config <path>` / `FRONTMCP_CONFIG` branch so a
|
|
120
|
+
* legacy exec-only config passed via explicit path resolves the same way
|
|
121
|
+
* an auto-discovered one does: `config: undefined`, no throw, callers fall
|
|
122
|
+
* back to `loadExecConfig`. Real parse failures still propagate.
|
|
123
|
+
*/
|
|
124
|
+
async function tryLoadFrontMcpConfigFromFile(configPath) {
|
|
125
|
+
const absolutePath = (0, utils_1.isAbsolute)(configPath) ? configPath : (0, utils_1.pathResolve)(process.cwd(), configPath);
|
|
126
|
+
if (!(await (0, utils_1.fileExists)(absolutePath))) {
|
|
127
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
128
|
+
}
|
|
129
|
+
const filename = (0, utils_1.basename)(absolutePath);
|
|
130
|
+
const raw = await loadRawFileAtPath(absolutePath, filename);
|
|
131
|
+
return parseRawOrLegacy(raw);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Schema-validate a raw config payload, returning `undefined` when it
|
|
135
|
+
* matches the legacy exec-only shape (top-level `cli` / `sea` / `esbuild`,
|
|
136
|
+
* no `deployments`) and throwing on every other parse failure. Shared
|
|
137
|
+
* between the cwd-search and explicit-path soft loaders so they agree on
|
|
138
|
+
* what "legacy" means.
|
|
139
|
+
*/
|
|
140
|
+
function parseRawOrLegacy(raw) {
|
|
73
141
|
const result = frontmcp_config_schema_1.frontmcpConfigSchema.safeParse(raw);
|
|
74
142
|
if (!result.success) {
|
|
75
143
|
// Distinguish two failure shapes:
|
|
@@ -94,76 +162,87 @@ async function tryLoadFrontMcpConfig(cwd) {
|
|
|
94
162
|
*/
|
|
95
163
|
async function loadRawConfig(cwd) {
|
|
96
164
|
for (const filename of CONFIG_FILENAMES) {
|
|
97
|
-
const configPath =
|
|
98
|
-
if (!
|
|
165
|
+
const configPath = (0, utils_1.pathJoin)(cwd, filename);
|
|
166
|
+
if (!(await (0, utils_1.fileExists)(configPath)))
|
|
99
167
|
continue;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let requireErr;
|
|
132
|
-
try {
|
|
133
|
-
const mod = require(configPath);
|
|
134
|
-
return mod.default ?? mod;
|
|
135
|
-
}
|
|
136
|
-
catch (e) {
|
|
137
|
-
requireErr = e;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
const mod = await import(configPath);
|
|
141
|
-
return mod.default ?? mod;
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
// Fall through to esbuild.
|
|
145
|
-
}
|
|
168
|
+
return loadRawFileAtPath(configPath, filename);
|
|
169
|
+
}
|
|
170
|
+
// Fallback: derive from package.json
|
|
171
|
+
return deriveFromPackageJson(cwd);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Load a single config file by absolute path (no search). Shared by
|
|
175
|
+
* `loadRawConfig` (search-then-load) and `loadFrontMcpConfigFromFile`
|
|
176
|
+
* (explicit-path).
|
|
177
|
+
*/
|
|
178
|
+
async function loadRawFileAtPath(configPath, filename) {
|
|
179
|
+
if (filename.endsWith('.json')) {
|
|
180
|
+
const content = await (0, utils_1.readFile)(configPath);
|
|
181
|
+
return JSON.parse(content);
|
|
182
|
+
}
|
|
183
|
+
if (filename.endsWith('.ts')) {
|
|
184
|
+
const cwd = (0, utils_1.dirname)(configPath);
|
|
185
|
+
// #365 — Loading `.ts` under `"type": "commonjs"` (the default) is a
|
|
186
|
+
// minefield across Node versions:
|
|
187
|
+
// - Node 20: `require()` throws on TS syntax, `await import()` errors
|
|
188
|
+
// with "Make sure to set type: module".
|
|
189
|
+
// - Node 22+: `require(esm)` may succeed but return partial data, OR
|
|
190
|
+
// emit a warning on `await import()` even when the load succeeds.
|
|
191
|
+
// - Node 24: type-stripping may swallow `import { x } from ...`
|
|
192
|
+
// statements, returning `{}` instead of the user's exports — the
|
|
193
|
+
// 1.1.2-beta.1 silent-defaults regression.
|
|
194
|
+
// Round 3: under CJS, ALWAYS transpile via esbuild. It's the only path
|
|
195
|
+
// that produces a deterministic, fully-typed result. ESM projects can
|
|
196
|
+
// still use Node's runtime loaders since they're well-behaved there.
|
|
197
|
+
const isCjsProject = await isCommonJsProject(cwd);
|
|
198
|
+
if (isCjsProject) {
|
|
146
199
|
try {
|
|
147
200
|
return await loadTsConfigViaEsbuild(configPath);
|
|
148
201
|
}
|
|
149
202
|
catch (esbuildErr) {
|
|
150
|
-
throw new Error(`Failed to load ${filename}.\n` +
|
|
151
|
-
`
|
|
152
|
-
` esbuild error: ${esbuildErr.message}\n` +
|
|
203
|
+
throw new Error(`Failed to load ${filename} via esbuild.\n` +
|
|
204
|
+
` ${esbuildErr.message}\n` +
|
|
153
205
|
`Hint: ensure the file exports a default config (e.g., ` +
|
|
154
206
|
`\`export default defineConfig({...})\`) and that all imports resolve.`);
|
|
155
207
|
}
|
|
156
208
|
}
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
209
|
+
// ESM project ("type": "module"): try Node's loaders first (faster,
|
|
210
|
+
// no transpile cost), fall back to esbuild on failure.
|
|
211
|
+
let requireErr;
|
|
212
|
+
try {
|
|
213
|
+
const mod = require(configPath);
|
|
214
|
+
return mod.default ?? mod;
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
requireErr = e;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
// pathToFileURL — Windows absolute paths (e.g. `C:\…\frontmcp.config.ts`)
|
|
221
|
+
// are not valid ESM specifiers; Node requires a `file://` URL.
|
|
222
|
+
const mod = await import((0, utils_1.pathToFileURL)(configPath).href);
|
|
160
223
|
return mod.default ?? mod;
|
|
161
224
|
}
|
|
162
|
-
|
|
225
|
+
catch {
|
|
226
|
+
// Fall through to esbuild.
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
return await loadTsConfigViaEsbuild(configPath);
|
|
230
|
+
}
|
|
231
|
+
catch (esbuildErr) {
|
|
232
|
+
throw new Error(`Failed to load ${filename}.\n` +
|
|
233
|
+
` require() error: ${requireErr?.message ?? '(skipped)'}\n` +
|
|
234
|
+
` esbuild error: ${esbuildErr.message}\n` +
|
|
235
|
+
`Hint: ensure the file exports a default config (e.g., ` +
|
|
236
|
+
`\`export default defineConfig({...})\`) and that all imports resolve.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// JS/MJS/CJS
|
|
240
|
+
if (filename.endsWith('.mjs')) {
|
|
241
|
+
const mod = await import((0, utils_1.pathToFileURL)(configPath).href);
|
|
163
242
|
return mod.default ?? mod;
|
|
164
243
|
}
|
|
165
|
-
|
|
166
|
-
return
|
|
244
|
+
const mod = require(configPath);
|
|
245
|
+
return mod.default ?? mod;
|
|
167
246
|
}
|
|
168
247
|
/**
|
|
169
248
|
* Read the host project's `package.json.type` to decide whether `await import()`
|
|
@@ -179,7 +258,7 @@ async function loadRawConfig(cwd) {
|
|
|
179
258
|
*/
|
|
180
259
|
async function isCommonJsProject(cwd) {
|
|
181
260
|
try {
|
|
182
|
-
const pkgPath =
|
|
261
|
+
const pkgPath = (0, utils_1.pathJoin)(cwd, 'package.json');
|
|
183
262
|
const contents = await (0, utils_1.readFile)(pkgPath);
|
|
184
263
|
const pkg = JSON.parse(contents);
|
|
185
264
|
return pkg.type !== 'module';
|
|
@@ -226,7 +305,7 @@ async function loadTsConfigViaEsbuild(configPath) {
|
|
|
226
305
|
// Make the loaded module's `require` resolve relative to the config dir
|
|
227
306
|
// so user `import { defineConfig } from 'frontmcp'` keeps working.
|
|
228
307
|
m.filename = configPath;
|
|
229
|
-
m.paths = Module._nodeModulePaths(
|
|
308
|
+
m.paths = Module._nodeModulePaths((0, utils_1.dirname)(configPath));
|
|
230
309
|
m._compile(code, configPath);
|
|
231
310
|
const exported = m.exports;
|
|
232
311
|
return exported?.default ?? exported;
|
|
@@ -234,14 +313,14 @@ async function loadTsConfigViaEsbuild(configPath) {
|
|
|
234
313
|
/**
|
|
235
314
|
* Derive minimal config from package.json.
|
|
236
315
|
*/
|
|
237
|
-
function deriveFromPackageJson(cwd) {
|
|
238
|
-
const pkgPath =
|
|
239
|
-
if (!
|
|
316
|
+
async function deriveFromPackageJson(cwd) {
|
|
317
|
+
const pkgPath = (0, utils_1.pathJoin)(cwd, 'package.json');
|
|
318
|
+
if (!(await (0, utils_1.fileExists)(pkgPath))) {
|
|
240
319
|
throw new Error('No frontmcp.config found and no package.json. Create a frontmcp.config.ts to configure build targets.');
|
|
241
320
|
}
|
|
242
|
-
const pkg = JSON.parse(
|
|
321
|
+
const pkg = JSON.parse(await (0, utils_1.readFile)(pkgPath));
|
|
243
322
|
return {
|
|
244
|
-
name: pkg.name?.replace(/^@[^/]+\//, '') ||
|
|
323
|
+
name: pkg.name?.replace(/^@[^/]+\//, '') || (0, utils_1.basename)(cwd),
|
|
245
324
|
version: pkg.version || '1.0.0',
|
|
246
325
|
entry: pkg.main,
|
|
247
326
|
deployments: [{ target: 'node' }],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontmcp-config.loader.js","sourceRoot":"","sources":["../../../src/config/frontmcp-config.loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FrontMCP Config Loader\n *\n * Loads `frontmcp.config.(json|js|ts|mjs|cjs)` from a directory.\n * Falls back to deriving minimal config from package.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { readFile } from '@frontmcp/utils';\n\nimport { frontmcpConfigSchema, type FrontMcpConfigParsed } from './frontmcp-config.schema';\nimport type { DeploymentTarget, FrontMcpConfig } from './frontmcp-config.types';\n\nconst CONFIG_FILENAMES = [\n 'frontmcp.config.ts',\n 'frontmcp.config.js',\n 'frontmcp.config.json',\n 'frontmcp.config.mjs',\n 'frontmcp.config.cjs',\n];\n\n/**\n * Load and validate a frontmcp.config file from the given directory.\n *\n * Resolution order:\n * 1. frontmcp.config.ts\n * 2. frontmcp.config.js\n * 3. frontmcp.config.json\n * 4. frontmcp.config.mjs\n * 5. frontmcp.config.cjs\n * 6. Derive from package.json (minimal config with 'node' target)\n */\nexport async function loadFrontMcpConfig(cwd: string): Promise<FrontMcpConfigParsed> {\n const raw = await loadRawConfig(cwd);\n return validateConfig(raw);\n}\n\n/**\n * Variant that load-errors propagate (parse failures in `frontmcp.config.ts`,\n * missing dependencies, etc.) but schema-validation errors return `undefined`.\n *\n * Used by `runBuild` to support both shapes: the new top-level\n * `frontmcpConfigSchema` (with `deployments`) and the older exec-only shape\n * (top-level `cli`, `sea`, `esbuild`) that `loadExecConfig` consumes\n * directly. A user with the old shape should still get a successful build\n * — the exec-loader picks the file up from disk by itself.\n *\n * Returns `undefined` when:\n * - no config file is present (caller falls back to CLI flags), or\n * - the file loads but doesn't match the new schema (legacy shape).\n *\n * Throws when:\n * - the file exists but can't be parsed (TS syntax error, ESM/CJS mismatch\n * that even esbuild can't recover from, etc.) — i.e., #365 silent-default\n * regressions.\n */\nexport async function tryLoadFrontMcpConfig(cwd: string): Promise<FrontMcpConfigParsed | undefined> {\n let raw: unknown;\n try {\n raw = await loadRawConfig(cwd);\n } catch (err) {\n // Distinguish \"no config and no package.json\" from real load failures.\n // `deriveFromPackageJson` throws this exact message — treat it as \"no config\".\n if ((err as Error).message?.startsWith('No frontmcp.config found')) {\n return undefined;\n }\n throw err;\n }\n const result = frontmcpConfigSchema.safeParse(raw);\n if (!result.success) {\n // Distinguish two failure shapes:\n // 1. Legacy exec-only config — top-level `cli` / `sea` / `esbuild`,\n // no `deployments` key. Return undefined so `loadExecConfig` picks\n // it up directly. Pre-v1.1 fixtures live in this branch.\n // 2. Anything else — looks like the user attempted a v1.1 config and\n // got it wrong (typo'd `deployments`, invalid `target`, etc.).\n // Throw so we don't silently fall back to defaults — that was the\n // silent-corruption mode #365 was trying to eliminate.\n const obj = typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : undefined;\n const isLegacyExecOnlyShape =\n !!obj && !('deployments' in obj) && ('cli' in obj || 'sea' in obj || 'esbuild' in obj);\n if (isLegacyExecOnlyShape) return undefined;\n const issues = result.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\\n');\n throw new Error(`Invalid frontmcp.config:\\n${issues}`);\n }\n return result.data;\n}\n\n/**\n * Load raw config without validation.\n */\nasync function loadRawConfig(cwd: string): Promise<unknown> {\n for (const filename of CONFIG_FILENAMES) {\n const configPath = path.join(cwd, filename);\n if (!fs.existsSync(configPath)) continue;\n\n if (filename.endsWith('.json')) {\n const content = fs.readFileSync(configPath, 'utf-8');\n return JSON.parse(content);\n }\n\n if (filename.endsWith('.ts')) {\n // #365 — Loading `.ts` under `\"type\": \"commonjs\"` (the default) is a\n // minefield across Node versions:\n // - Node 20: `require()` throws on TS syntax, `await import()` errors\n // with \"Make sure to set type: module\".\n // - Node 22+: `require(esm)` may succeed but return partial data, OR\n // emit a warning on `await import()` even when the load succeeds.\n // - Node 24: type-stripping may swallow `import { x } from ...`\n // statements, returning `{}` instead of the user's exports — the\n // 1.1.2-beta.1 silent-defaults regression.\n // Round 3: under CJS, ALWAYS transpile via esbuild. It's the only path\n // that produces a deterministic, fully-typed result. ESM projects can\n // still use Node's runtime loaders since they're well-behaved there.\n const isCjsProject = await isCommonJsProject(cwd);\n if (isCjsProject) {\n try {\n return await loadTsConfigViaEsbuild(configPath);\n } catch (esbuildErr) {\n throw new Error(\n `Failed to load ${filename} via esbuild.\\n` +\n ` ${(esbuildErr as Error).message}\\n` +\n `Hint: ensure the file exports a default config (e.g., ` +\n `\\`export default defineConfig({...})\\`) and that all imports resolve.`,\n );\n }\n }\n // ESM project (\"type\": \"module\"): try Node's loaders first (faster,\n // no transpile cost), fall back to esbuild on failure.\n let requireErr: Error | undefined;\n try {\n const mod = require(configPath);\n return mod.default ?? mod;\n } catch (e) {\n requireErr = e as Error;\n }\n try {\n const mod = await import(configPath);\n return mod.default ?? mod;\n } catch {\n // Fall through to esbuild.\n }\n try {\n return await loadTsConfigViaEsbuild(configPath);\n } catch (esbuildErr) {\n throw new Error(\n `Failed to load ${filename}.\\n` +\n ` require() error: ${requireErr?.message ?? '(skipped)'}\\n` +\n ` esbuild error: ${(esbuildErr as Error).message}\\n` +\n `Hint: ensure the file exports a default config (e.g., ` +\n `\\`export default defineConfig({...})\\`) and that all imports resolve.`,\n );\n }\n }\n\n // JS/MJS/CJS\n if (filename.endsWith('.mjs')) {\n const mod = await import(configPath);\n return mod.default ?? mod;\n }\n\n const mod = require(configPath);\n return mod.default ?? mod;\n }\n\n // Fallback: derive from package.json\n return deriveFromPackageJson(cwd);\n}\n\n/**\n * Read the host project's `package.json.type` to decide whether `await import()`\n * of a `.ts` file is worth attempting. Returns true when the project is\n * declared `\"type\": \"commonjs\"` or omits the field entirely (Node's default).\n *\n * Read errors (no package.json, malformed JSON) are treated as \"CJS\" — that's\n * the safer default for the loader because it routes us through esbuild\n * transpilation rather than relying on Node's experimental TS handling.\n *\n * Routed through `@frontmcp/utils` per repo convention so this module\n * doesn't reach into `node:fs` for an ad-hoc package.json read.\n */\nasync function isCommonJsProject(cwd: string): Promise<boolean> {\n try {\n const pkgPath = path.join(cwd, 'package.json');\n const contents = await readFile(pkgPath);\n const pkg = JSON.parse(contents) as { type?: string };\n return pkg.type !== 'module';\n } catch {\n return true;\n }\n}\n\n/**\n * Transpile a TypeScript config file with esbuild (CJS target) and eval the\n * result via Module-via-vm. Used as a last-resort path when neither `require()`\n * (no ts-node hook) nor `await import()` (project is `\"type\": \"commonjs\"`)\n * can load the file directly.\n *\n * Uses `esbuild.build({ bundle: true, packages: 'external' })` rather than\n * `transformSync` so a config that imports a sibling helper TS file\n * (`import { foo } from './helpers'`) gets the helper inlined into the\n * compiled output. Without bundling, the resulting CJS would emit\n * `require('./helpers')` which Node can't resolve under `\"type\": \"commonjs\"`.\n *\n * `packages: 'external'` keeps node_modules dependencies as runtime\n * `require()` calls so `import { defineConfig } from 'frontmcp'` still\n * resolves against the project's installed copy of the SDK.\n */\nasync function loadTsConfigViaEsbuild(configPath: string): Promise<unknown> {\n const esbuild = require('esbuild') as typeof import('esbuild');\n const built = await esbuild.build({\n entryPoints: [configPath],\n bundle: true,\n write: false,\n platform: 'node',\n format: 'cjs',\n target: 'es2022',\n packages: 'external',\n sourcemap: 'inline',\n logLevel: 'silent',\n });\n if (!built.outputFiles || built.outputFiles.length === 0) {\n throw new Error('esbuild produced no output for ' + configPath);\n }\n const code = built.outputFiles[0].text;\n\n const Module = require('module') as typeof import('module');\n const m = new Module(configPath, module);\n // Make the loaded module's `require` resolve relative to the config dir\n // so user `import { defineConfig } from 'frontmcp'` keeps working.\n m.filename = configPath;\n m.paths = (Module as unknown as { _nodeModulePaths(p: string): string[] })._nodeModulePaths(path.dirname(configPath));\n\n (m as any)._compile(code, configPath);\n\n const exported = (m as any).exports as { default?: unknown };\n return exported?.default ?? exported;\n}\n\n/**\n * Derive minimal config from package.json.\n */\nfunction deriveFromPackageJson(cwd: string): FrontMcpConfig {\n const pkgPath = path.join(cwd, 'package.json');\n if (!fs.existsSync(pkgPath)) {\n throw new Error(\n 'No frontmcp.config found and no package.json. Create a frontmcp.config.ts to configure build targets.',\n );\n }\n\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n return {\n name: pkg.name?.replace(/^@[^/]+\\//, '') || path.basename(cwd),\n version: pkg.version || '1.0.0',\n entry: pkg.main,\n deployments: [{ target: 'node' }],\n };\n}\n\n/**\n * Validate raw config against the Zod schema.\n */\nexport function validateConfig(raw: unknown): FrontMcpConfigParsed {\n const result = frontmcpConfigSchema.safeParse(raw);\n if (!result.success) {\n const issues = result.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\\n');\n throw new Error(`Invalid frontmcp.config:\\n${issues}`);\n }\n return result.data;\n}\n\n/**\n * Find a deployment target by type.\n */\nexport function findDeployment(config: FrontMcpConfigParsed, target: string): DeploymentTarget | undefined {\n return config.deployments.find((d) => d.target === target);\n}\n\n/**\n * Get all deployment target types from the config.\n */\nexport function getDeploymentTargets(config: FrontMcpConfigParsed): string[] {\n return config.deployments.map((d) => d.target);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"frontmcp-config.loader.js","sourceRoot":"","sources":["../../../src/config/frontmcp-config.loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FrontMCP Config Loader\n *\n * Loads `frontmcp.config.(json|js|ts|mjs|cjs)` from a directory.\n * Falls back to deriving minimal config from package.json.\n */\n\nimport {\n basename,\n dirname,\n fileExists,\n isAbsolute,\n pathJoin,\n pathResolve,\n pathToFileURL,\n readFile,\n} from '@frontmcp/utils';\n\nimport { frontmcpConfigSchema, type FrontMcpConfigParsed } from './frontmcp-config.schema';\nimport type { DeploymentTarget, FrontMcpConfig } from './frontmcp-config.types';\n\nconst CONFIG_FILENAMES = [\n 'frontmcp.config.ts',\n 'frontmcp.config.js',\n 'frontmcp.config.json',\n 'frontmcp.config.mjs',\n 'frontmcp.config.cjs',\n];\n\n/**\n * Load and validate a frontmcp.config file from the given directory.\n *\n * Resolution order:\n * 1. frontmcp.config.ts\n * 2. frontmcp.config.js\n * 3. frontmcp.config.json\n * 4. frontmcp.config.mjs\n * 5. frontmcp.config.cjs\n * 6. Derive from package.json (minimal config with 'node' target)\n */\nexport async function loadFrontMcpConfig(cwd: string): Promise<FrontMcpConfigParsed> {\n const raw = await loadRawConfig(cwd);\n return validateConfig(raw);\n}\n\n/**\n * Load a specific config file by absolute or cwd-relative path. Used by the\n * `--config <path>` flag and the `FRONTMCP_CONFIG` env var (issue #400).\n *\n * Unlike `loadFrontMcpConfig`, this doesn't search `CONFIG_FILENAMES` — the\n * caller already named the file, so a missing-file error is a hard failure\n * (no silent fallback to `deriveFromPackageJson`).\n */\nexport async function loadFrontMcpConfigFromFile(configPath: string): Promise<FrontMcpConfigParsed> {\n const absolutePath = isAbsolute(configPath) ? configPath : pathResolve(process.cwd(), configPath);\n if (!(await fileExists(absolutePath))) {\n throw new Error(`Config file not found: ${configPath}`);\n }\n const filename = basename(absolutePath);\n const raw = await loadRawFileAtPath(absolutePath, filename);\n return validateConfig(raw);\n}\n\n/**\n * Locate the nearest `frontmcp.config.*` file by walking upward from `cwd`.\n *\n * Issue #400 — monorepo nested apps no longer require `cd <repo-root>`\n * before invoking the CLI. The walk caps at 10 levels to avoid pathological\n * symlink loops.\n *\n * Returns the directory containing the config (so callers can pass it to\n * `loadFrontMcpConfig(dir)`), or `undefined` if nothing was found.\n */\nexport async function findConfigDir(startDir: string, maxLevels = 10): Promise<string | undefined> {\n let current = pathResolve(startDir);\n for (let i = 0; i <= maxLevels; i++) {\n for (const filename of CONFIG_FILENAMES) {\n if (await fileExists(pathJoin(current, filename))) {\n return current;\n }\n }\n const parent = dirname(current);\n if (parent === current) return undefined;\n current = parent;\n }\n return undefined;\n}\n\n/**\n * Variant that load-errors propagate (parse failures in `frontmcp.config.ts`,\n * missing dependencies, etc.) but schema-validation errors return `undefined`.\n *\n * Used by `runBuild` to support both shapes: the new top-level\n * `frontmcpConfigSchema` (with `deployments`) and the older exec-only shape\n * (top-level `cli`, `sea`, `esbuild`) that `loadExecConfig` consumes\n * directly. A user with the old shape should still get a successful build\n * — the exec-loader picks the file up from disk by itself.\n *\n * Returns `undefined` when:\n * - no config file is present (caller falls back to CLI flags), or\n * - the file loads but doesn't match the new schema (legacy shape).\n *\n * Throws when:\n * - the file exists but can't be parsed (TS syntax error, ESM/CJS mismatch\n * that even esbuild can't recover from, etc.) — i.e., #365 silent-default\n * regressions.\n */\nexport async function tryLoadFrontMcpConfig(cwd: string): Promise<FrontMcpConfigParsed | undefined> {\n let raw: unknown;\n try {\n raw = await loadRawConfig(cwd);\n } catch (err) {\n // Distinguish \"no config and no package.json\" from real load failures.\n // `deriveFromPackageJson` throws this exact message — treat it as \"no config\".\n if ((err as Error).message?.startsWith('No frontmcp.config found')) {\n return undefined;\n }\n throw err;\n }\n return parseRawOrLegacy(raw);\n}\n\n/**\n * Explicit-path counterpart of {@link tryLoadFrontMcpConfig}. Used by\n * `resolveConfig` for the `--config <path>` / `FRONTMCP_CONFIG` branch so a\n * legacy exec-only config passed via explicit path resolves the same way\n * an auto-discovered one does: `config: undefined`, no throw, callers fall\n * back to `loadExecConfig`. Real parse failures still propagate.\n */\nexport async function tryLoadFrontMcpConfigFromFile(configPath: string): Promise<FrontMcpConfigParsed | undefined> {\n const absolutePath = isAbsolute(configPath) ? configPath : pathResolve(process.cwd(), configPath);\n if (!(await fileExists(absolutePath))) {\n throw new Error(`Config file not found: ${configPath}`);\n }\n const filename = basename(absolutePath);\n const raw = await loadRawFileAtPath(absolutePath, filename);\n return parseRawOrLegacy(raw);\n}\n\n/**\n * Schema-validate a raw config payload, returning `undefined` when it\n * matches the legacy exec-only shape (top-level `cli` / `sea` / `esbuild`,\n * no `deployments`) and throwing on every other parse failure. Shared\n * between the cwd-search and explicit-path soft loaders so they agree on\n * what \"legacy\" means.\n */\nfunction parseRawOrLegacy(raw: unknown): FrontMcpConfigParsed | undefined {\n const result = frontmcpConfigSchema.safeParse(raw);\n if (!result.success) {\n // Distinguish two failure shapes:\n // 1. Legacy exec-only config — top-level `cli` / `sea` / `esbuild`,\n // no `deployments` key. Return undefined so `loadExecConfig` picks\n // it up directly. Pre-v1.1 fixtures live in this branch.\n // 2. Anything else — looks like the user attempted a v1.1 config and\n // got it wrong (typo'd `deployments`, invalid `target`, etc.).\n // Throw so we don't silently fall back to defaults — that was the\n // silent-corruption mode #365 was trying to eliminate.\n const obj = typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : undefined;\n const isLegacyExecOnlyShape =\n !!obj && !('deployments' in obj) && ('cli' in obj || 'sea' in obj || 'esbuild' in obj);\n if (isLegacyExecOnlyShape) return undefined;\n const issues = result.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\\n');\n throw new Error(`Invalid frontmcp.config:\\n${issues}`);\n }\n return result.data;\n}\n\n/**\n * Load raw config without validation.\n */\nasync function loadRawConfig(cwd: string): Promise<unknown> {\n for (const filename of CONFIG_FILENAMES) {\n const configPath = pathJoin(cwd, filename);\n if (!(await fileExists(configPath))) continue;\n return loadRawFileAtPath(configPath, filename);\n }\n\n // Fallback: derive from package.json\n return deriveFromPackageJson(cwd);\n}\n\n/**\n * Load a single config file by absolute path (no search). Shared by\n * `loadRawConfig` (search-then-load) and `loadFrontMcpConfigFromFile`\n * (explicit-path).\n */\nasync function loadRawFileAtPath(configPath: string, filename: string): Promise<unknown> {\n if (filename.endsWith('.json')) {\n const content = await readFile(configPath);\n return JSON.parse(content);\n }\n\n if (filename.endsWith('.ts')) {\n const cwd = dirname(configPath);\n // #365 — Loading `.ts` under `\"type\": \"commonjs\"` (the default) is a\n // minefield across Node versions:\n // - Node 20: `require()` throws on TS syntax, `await import()` errors\n // with \"Make sure to set type: module\".\n // - Node 22+: `require(esm)` may succeed but return partial data, OR\n // emit a warning on `await import()` even when the load succeeds.\n // - Node 24: type-stripping may swallow `import { x } from ...`\n // statements, returning `{}` instead of the user's exports — the\n // 1.1.2-beta.1 silent-defaults regression.\n // Round 3: under CJS, ALWAYS transpile via esbuild. It's the only path\n // that produces a deterministic, fully-typed result. ESM projects can\n // still use Node's runtime loaders since they're well-behaved there.\n const isCjsProject = await isCommonJsProject(cwd);\n if (isCjsProject) {\n try {\n return await loadTsConfigViaEsbuild(configPath);\n } catch (esbuildErr) {\n throw new Error(\n `Failed to load ${filename} via esbuild.\\n` +\n ` ${(esbuildErr as Error).message}\\n` +\n `Hint: ensure the file exports a default config (e.g., ` +\n `\\`export default defineConfig({...})\\`) and that all imports resolve.`,\n );\n }\n }\n // ESM project (\"type\": \"module\"): try Node's loaders first (faster,\n // no transpile cost), fall back to esbuild on failure.\n let requireErr: Error | undefined;\n try {\n const mod = require(configPath);\n return mod.default ?? mod;\n } catch (e) {\n requireErr = e as Error;\n }\n try {\n // pathToFileURL — Windows absolute paths (e.g. `C:\\…\\frontmcp.config.ts`)\n // are not valid ESM specifiers; Node requires a `file://` URL.\n const mod = await import(pathToFileURL(configPath).href);\n return mod.default ?? mod;\n } catch {\n // Fall through to esbuild.\n }\n try {\n return await loadTsConfigViaEsbuild(configPath);\n } catch (esbuildErr) {\n throw new Error(\n `Failed to load ${filename}.\\n` +\n ` require() error: ${requireErr?.message ?? '(skipped)'}\\n` +\n ` esbuild error: ${(esbuildErr as Error).message}\\n` +\n `Hint: ensure the file exports a default config (e.g., ` +\n `\\`export default defineConfig({...})\\`) and that all imports resolve.`,\n );\n }\n }\n\n // JS/MJS/CJS\n if (filename.endsWith('.mjs')) {\n const mod = await import(pathToFileURL(configPath).href);\n return mod.default ?? mod;\n }\n\n const mod = require(configPath);\n return mod.default ?? mod;\n}\n\n/**\n * Read the host project's `package.json.type` to decide whether `await import()`\n * of a `.ts` file is worth attempting. Returns true when the project is\n * declared `\"type\": \"commonjs\"` or omits the field entirely (Node's default).\n *\n * Read errors (no package.json, malformed JSON) are treated as \"CJS\" — that's\n * the safer default for the loader because it routes us through esbuild\n * transpilation rather than relying on Node's experimental TS handling.\n *\n * Routed through `@frontmcp/utils` per repo convention so this module\n * doesn't reach into `node:fs` for an ad-hoc package.json read.\n */\nasync function isCommonJsProject(cwd: string): Promise<boolean> {\n try {\n const pkgPath = pathJoin(cwd, 'package.json');\n const contents = await readFile(pkgPath);\n const pkg = JSON.parse(contents) as { type?: string };\n return pkg.type !== 'module';\n } catch {\n return true;\n }\n}\n\n/**\n * Transpile a TypeScript config file with esbuild (CJS target) and eval the\n * result via Module-via-vm. Used as a last-resort path when neither `require()`\n * (no ts-node hook) nor `await import()` (project is `\"type\": \"commonjs\"`)\n * can load the file directly.\n *\n * Uses `esbuild.build({ bundle: true, packages: 'external' })` rather than\n * `transformSync` so a config that imports a sibling helper TS file\n * (`import { foo } from './helpers'`) gets the helper inlined into the\n * compiled output. Without bundling, the resulting CJS would emit\n * `require('./helpers')` which Node can't resolve under `\"type\": \"commonjs\"`.\n *\n * `packages: 'external'` keeps node_modules dependencies as runtime\n * `require()` calls so `import { defineConfig } from 'frontmcp'` still\n * resolves against the project's installed copy of the SDK.\n */\nasync function loadTsConfigViaEsbuild(configPath: string): Promise<unknown> {\n const esbuild = require('esbuild') as typeof import('esbuild');\n const built = await esbuild.build({\n entryPoints: [configPath],\n bundle: true,\n write: false,\n platform: 'node',\n format: 'cjs',\n target: 'es2022',\n packages: 'external',\n sourcemap: 'inline',\n logLevel: 'silent',\n });\n if (!built.outputFiles || built.outputFiles.length === 0) {\n throw new Error('esbuild produced no output for ' + configPath);\n }\n const code = built.outputFiles[0].text;\n\n const Module = require('module') as typeof import('module');\n const m = new Module(configPath, module);\n // Make the loaded module's `require` resolve relative to the config dir\n // so user `import { defineConfig } from 'frontmcp'` keeps working.\n m.filename = configPath;\n m.paths = (Module as unknown as { _nodeModulePaths(p: string): string[] })._nodeModulePaths(dirname(configPath));\n\n (m as any)._compile(code, configPath);\n\n const exported = (m as any).exports as { default?: unknown };\n return exported?.default ?? exported;\n}\n\n/**\n * Derive minimal config from package.json.\n */\nasync function deriveFromPackageJson(cwd: string): Promise<FrontMcpConfig> {\n const pkgPath = pathJoin(cwd, 'package.json');\n if (!(await fileExists(pkgPath))) {\n throw new Error(\n 'No frontmcp.config found and no package.json. Create a frontmcp.config.ts to configure build targets.',\n );\n }\n\n const pkg = JSON.parse(await readFile(pkgPath));\n return {\n name: pkg.name?.replace(/^@[^/]+\\//, '') || basename(cwd),\n version: pkg.version || '1.0.0',\n entry: pkg.main,\n deployments: [{ target: 'node' }],\n };\n}\n\n/**\n * Validate raw config against the Zod schema.\n */\nexport function validateConfig(raw: unknown): FrontMcpConfigParsed {\n const result = frontmcpConfigSchema.safeParse(raw);\n if (!result.success) {\n const issues = result.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\\n');\n throw new Error(`Invalid frontmcp.config:\\n${issues}`);\n }\n return result.data;\n}\n\n/**\n * Find a deployment target by type.\n */\nexport function findDeployment(config: FrontMcpConfigParsed, target: string): DeploymentTarget | undefined {\n return config.deployments.find((d) => d.target === target);\n}\n\n/**\n * Get all deployment target types from the config.\n */\nexport function getDeploymentTargets(config: FrontMcpConfigParsed): string[] {\n return config.deployments.map((d) => d.target);\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified config resolver (issue #400).
|
|
3
|
+
*
|
|
4
|
+
* Single entry point each CLI command calls. Applies the precedence rules:
|
|
5
|
+
*
|
|
6
|
+
* explicit CLI flag > FRONTMCP_<NAME> env var > frontmcp.config field > built-in default
|
|
7
|
+
*
|
|
8
|
+
* and returns a `ResolvedFrontMcpConfig` with all defaults applied, the
|
|
9
|
+
* chosen deployment merged in, env overlays composed, and per-command
|
|
10
|
+
* fields surfaced.
|
|
11
|
+
*
|
|
12
|
+
* Config-file resolution order (matches the plan's Phase 2):
|
|
13
|
+
* 1. Explicit `--config <path>` CLI flag.
|
|
14
|
+
* 2. `FRONTMCP_CONFIG` env var.
|
|
15
|
+
* 3. Upward walk from `cwd` to the nearest `frontmcp.config.*`.
|
|
16
|
+
* 4. None — caller uses defaults / falls back to package.json (handled
|
|
17
|
+
* by the existing `loadFrontMcpConfig` for the `build` command).
|
|
18
|
+
*
|
|
19
|
+
* Notes:
|
|
20
|
+
* - Resolve never throws when no config is present — it returns
|
|
21
|
+
* `{ config: undefined, ... }` with the merged env / transport values
|
|
22
|
+
* it could compute from CLI options.
|
|
23
|
+
* - The legacy exec-only config shape (top-level `cli` / `sea` /
|
|
24
|
+
* `esbuild`, no `deployments`) is still resolvable but produces a
|
|
25
|
+
* `config: undefined` so `loadExecConfig` can pick the file up.
|
|
26
|
+
*/
|
|
27
|
+
import { type FrontMcpConfigParsed } from './frontmcp-config.schema';
|
|
28
|
+
/** Modes per command — used to choose which env overlay to apply. */
|
|
29
|
+
export type ResolveMode = 'build:cli' | 'build:ship' | 'dev' | 'test' | 'inspector' | 'pm:start' | 'pm:socket' | 'skills';
|
|
30
|
+
export interface ResolveConfigOptions {
|
|
31
|
+
/** Working directory the command was invoked from. */
|
|
32
|
+
cwd: string;
|
|
33
|
+
/** Effective command (drives env-overlay selection). */
|
|
34
|
+
mode: ResolveMode;
|
|
35
|
+
/** Explicit `--config <path>` from the CLI. */
|
|
36
|
+
configPath?: string;
|
|
37
|
+
/** Environment vars at invocation time (defaults to `process.env`). */
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
/** Already-parsed CLI options — values here win over the config. */
|
|
40
|
+
cliOptions?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface ResolvedFrontMcpConfig {
|
|
43
|
+
/**
|
|
44
|
+
* Parsed config when one was located + matched the schema. `undefined`
|
|
45
|
+
* means "no config file found" or "file matched the legacy exec-only
|
|
46
|
+
* shape" — callers must fall back to CLI/built-in defaults.
|
|
47
|
+
*/
|
|
48
|
+
config?: FrontMcpConfigParsed;
|
|
49
|
+
/** Directory that contained the resolved config. */
|
|
50
|
+
configDir?: string;
|
|
51
|
+
/** Absolute path to the resolved config file. */
|
|
52
|
+
configPath?: string;
|
|
53
|
+
/**
|
|
54
|
+
* `process.env` ⊕ `config.env.shared` ⊕ `config.env.<mode>` ⊕
|
|
55
|
+
* `cliOptions.env` (later wins). `.env`/`.env.local` are NOT applied
|
|
56
|
+
* here — `dev`/`test` load those separately so they win for parity
|
|
57
|
+
* with existing behavior.
|
|
58
|
+
*/
|
|
59
|
+
effectiveEnv: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the config + env for the current command.
|
|
63
|
+
*
|
|
64
|
+
* Side-effect-free — call sites apply the returned `effectiveEnv` to the
|
|
65
|
+
* spawned child themselves (`dev` adds `.env`/`.env.local` on top, etc.).
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveConfig(options: ResolveConfigOptions): Promise<ResolvedFrontMcpConfig>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unified config resolver (issue #400).
|
|
4
|
+
*
|
|
5
|
+
* Single entry point each CLI command calls. Applies the precedence rules:
|
|
6
|
+
*
|
|
7
|
+
* explicit CLI flag > FRONTMCP_<NAME> env var > frontmcp.config field > built-in default
|
|
8
|
+
*
|
|
9
|
+
* and returns a `ResolvedFrontMcpConfig` with all defaults applied, the
|
|
10
|
+
* chosen deployment merged in, env overlays composed, and per-command
|
|
11
|
+
* fields surfaced.
|
|
12
|
+
*
|
|
13
|
+
* Config-file resolution order (matches the plan's Phase 2):
|
|
14
|
+
* 1. Explicit `--config <path>` CLI flag.
|
|
15
|
+
* 2. `FRONTMCP_CONFIG` env var.
|
|
16
|
+
* 3. Upward walk from `cwd` to the nearest `frontmcp.config.*`.
|
|
17
|
+
* 4. None — caller uses defaults / falls back to package.json (handled
|
|
18
|
+
* by the existing `loadFrontMcpConfig` for the `build` command).
|
|
19
|
+
*
|
|
20
|
+
* Notes:
|
|
21
|
+
* - Resolve never throws when no config is present — it returns
|
|
22
|
+
* `{ config: undefined, ... }` with the merged env / transport values
|
|
23
|
+
* it could compute from CLI options.
|
|
24
|
+
* - The legacy exec-only config shape (top-level `cli` / `sea` /
|
|
25
|
+
* `esbuild`, no `deployments`) is still resolvable but produces a
|
|
26
|
+
* `config: undefined` so `loadExecConfig` can pick the file up.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.resolveConfig = resolveConfig;
|
|
30
|
+
const utils_1 = require("@frontmcp/utils");
|
|
31
|
+
const frontmcp_config_loader_1 = require("./frontmcp-config.loader");
|
|
32
|
+
/** Map a `ResolveMode` to the env-overlay key (`'dev'`, `'test'`, `'ship'`). */
|
|
33
|
+
function modeToEnvKey(mode) {
|
|
34
|
+
switch (mode) {
|
|
35
|
+
case 'dev':
|
|
36
|
+
case 'inspector':
|
|
37
|
+
return 'dev';
|
|
38
|
+
case 'test':
|
|
39
|
+
return 'test';
|
|
40
|
+
case 'build:ship':
|
|
41
|
+
case 'pm:start':
|
|
42
|
+
case 'pm:socket':
|
|
43
|
+
return 'ship';
|
|
44
|
+
case 'build:cli':
|
|
45
|
+
case 'skills':
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the config + env for the current command.
|
|
51
|
+
*
|
|
52
|
+
* Side-effect-free — call sites apply the returned `effectiveEnv` to the
|
|
53
|
+
* spawned child themselves (`dev` adds `.env`/`.env.local` on top, etc.).
|
|
54
|
+
*/
|
|
55
|
+
async function resolveConfig(options) {
|
|
56
|
+
const env = options.env ?? process.env;
|
|
57
|
+
// Only string values flow into the spawned child's env — non-strings
|
|
58
|
+
// here mean a misconfigured `cliOptions.env`, so we silently drop them
|
|
59
|
+
// rather than corrupting the merged record with `Object`/`number`/etc.
|
|
60
|
+
const cliEnvRaw = options.cliOptions?.['env'];
|
|
61
|
+
const cliEnv = typeof cliEnvRaw === 'object' && cliEnvRaw !== null
|
|
62
|
+
? Object.fromEntries(Object.entries(cliEnvRaw).filter((entry) => typeof entry[1] === 'string'))
|
|
63
|
+
: {};
|
|
64
|
+
// ── Locate the config file ──
|
|
65
|
+
const explicitPath = options.configPath ?? env['FRONTMCP_CONFIG'];
|
|
66
|
+
let config;
|
|
67
|
+
let configPath;
|
|
68
|
+
let configDir;
|
|
69
|
+
if (explicitPath) {
|
|
70
|
+
// Normalize to an absolute path so callers always see canonical metadata
|
|
71
|
+
// regardless of how the caller-supplied path was spelt (relative, absolute,
|
|
72
|
+
// or env-var-derived). `configDir` mirrors the auto-discovery branch.
|
|
73
|
+
configPath = (0, utils_1.isAbsolute)(explicitPath) ? explicitPath : (0, utils_1.pathResolve)(options.cwd, explicitPath);
|
|
74
|
+
configDir = (0, utils_1.dirname)(configPath);
|
|
75
|
+
try {
|
|
76
|
+
// Soft variant: legacy exec-only configs (top-level `cli` / `sea` /
|
|
77
|
+
// `esbuild`, no `deployments`) resolve to `config: undefined` so
|
|
78
|
+
// `loadExecConfig` can still pick them up — same behaviour as the
|
|
79
|
+
// auto-discovery branch below, per the file-header contract. Real
|
|
80
|
+
// parse/schema failures still propagate as a hard error.
|
|
81
|
+
config = await (0, frontmcp_config_loader_1.tryLoadFrontMcpConfigFromFile)(configPath);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
throw new Error(`Failed to load config from "${explicitPath}": ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
configDir = await (0, frontmcp_config_loader_1.findConfigDir)(options.cwd);
|
|
89
|
+
if (configDir) {
|
|
90
|
+
try {
|
|
91
|
+
config = await (0, frontmcp_config_loader_1.tryLoadFrontMcpConfig)(configDir);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Surface schema/load errors — silent fallback was the corruption
|
|
95
|
+
// mode #365 worked to eliminate. The legacy-shape branch inside
|
|
96
|
+
// `tryLoadFrontMcpConfig` already returns `undefined` for old
|
|
97
|
+
// exec-only configs, so anything reaching this catch is a real
|
|
98
|
+
// parse failure that the caller needs to see.
|
|
99
|
+
throw new Error(`Failed to load frontmcp.config in ${configDir}: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Compose effective env ──
|
|
104
|
+
const overlay = config?.env;
|
|
105
|
+
const modeKey = modeToEnvKey(options.mode);
|
|
106
|
+
const fromShared = overlay?.shared ?? {};
|
|
107
|
+
const fromMode = (modeKey && overlay?.[modeKey]) ?? {};
|
|
108
|
+
// Start from `process.env` so OS / CI / shell env still apply, then layer
|
|
109
|
+
// shared + mode overlays + CLI-supplied env (later wins).
|
|
110
|
+
const effectiveEnv = {};
|
|
111
|
+
for (const [key, value] of Object.entries(env)) {
|
|
112
|
+
if (typeof value === 'string')
|
|
113
|
+
effectiveEnv[key] = value;
|
|
114
|
+
}
|
|
115
|
+
Object.assign(effectiveEnv, fromShared, fromMode, cliEnv);
|
|
116
|
+
return { config, configDir, configPath, effectiveEnv };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=frontmcp-config.resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmcp-config.resolve.js","sourceRoot":"","sources":["../../../src/config/frontmcp-config.resolve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Unified config resolver (issue #400).\n *\n * Single entry point each CLI command calls. Applies the precedence rules:\n *\n * explicit CLI flag > FRONTMCP_<NAME> env var > frontmcp.config field > built-in default\n *\n * and returns a `ResolvedFrontMcpConfig` with all defaults applied, the\n * chosen deployment merged in, env overlays composed, and per-command\n * fields surfaced.\n *\n * Config-file resolution order (matches the plan's Phase 2):\n * 1. Explicit `--config <path>` CLI flag.\n * 2. `FRONTMCP_CONFIG` env var.\n * 3. Upward walk from `cwd` to the nearest `frontmcp.config.*`.\n * 4. None — caller uses defaults / falls back to package.json (handled\n * by the existing `loadFrontMcpConfig` for the `build` command).\n *\n * Notes:\n * - Resolve never throws when no config is present — it returns\n * `{ config: undefined, ... }` with the merged env / transport values\n * it could compute from CLI options.\n * - The legacy exec-only config shape (top-level `cli` / `sea` /\n * `esbuild`, no `deployments`) is still resolvable but produces a\n * `config: undefined` so `loadExecConfig` can pick the file up.\n */\n\nimport { dirname, isAbsolute, pathResolve } from '@frontmcp/utils';\n\nimport { findConfigDir, tryLoadFrontMcpConfig, tryLoadFrontMcpConfigFromFile } from './frontmcp-config.loader';\nimport { type FrontMcpConfigParsed } from './frontmcp-config.schema';\n\n/** Modes per command — used to choose which env overlay to apply. */\nexport type ResolveMode =\n | 'build:cli'\n | 'build:ship'\n | 'dev'\n | 'test'\n | 'inspector'\n | 'pm:start'\n | 'pm:socket'\n | 'skills';\n\nexport interface ResolveConfigOptions {\n /** Working directory the command was invoked from. */\n cwd: string;\n /** Effective command (drives env-overlay selection). */\n mode: ResolveMode;\n /** Explicit `--config <path>` from the CLI. */\n configPath?: string;\n /** Environment vars at invocation time (defaults to `process.env`). */\n env?: NodeJS.ProcessEnv;\n /** Already-parsed CLI options — values here win over the config. */\n cliOptions?: Record<string, unknown>;\n}\n\nexport interface ResolvedFrontMcpConfig {\n /**\n * Parsed config when one was located + matched the schema. `undefined`\n * means \"no config file found\" or \"file matched the legacy exec-only\n * shape\" — callers must fall back to CLI/built-in defaults.\n */\n config?: FrontMcpConfigParsed;\n /** Directory that contained the resolved config. */\n configDir?: string;\n /** Absolute path to the resolved config file. */\n configPath?: string;\n /**\n * `process.env` ⊕ `config.env.shared` ⊕ `config.env.<mode>` ⊕\n * `cliOptions.env` (later wins). `.env`/`.env.local` are NOT applied\n * here — `dev`/`test` load those separately so they win for parity\n * with existing behavior.\n */\n effectiveEnv: Record<string, string>;\n}\n\n/** Map a `ResolveMode` to the env-overlay key (`'dev'`, `'test'`, `'ship'`). */\nfunction modeToEnvKey(mode: ResolveMode): 'dev' | 'test' | 'ship' | undefined {\n switch (mode) {\n case 'dev':\n case 'inspector':\n return 'dev';\n case 'test':\n return 'test';\n case 'build:ship':\n case 'pm:start':\n case 'pm:socket':\n return 'ship';\n case 'build:cli':\n case 'skills':\n return undefined;\n }\n}\n\n/**\n * Resolve the config + env for the current command.\n *\n * Side-effect-free — call sites apply the returned `effectiveEnv` to the\n * spawned child themselves (`dev` adds `.env`/`.env.local` on top, etc.).\n */\nexport async function resolveConfig(options: ResolveConfigOptions): Promise<ResolvedFrontMcpConfig> {\n const env = options.env ?? process.env;\n // Only string values flow into the spawned child's env — non-strings\n // here mean a misconfigured `cliOptions.env`, so we silently drop them\n // rather than corrupting the merged record with `Object`/`number`/etc.\n const cliEnvRaw = options.cliOptions?.['env'];\n const cliEnv: Record<string, string> =\n typeof cliEnvRaw === 'object' && cliEnvRaw !== null\n ? Object.fromEntries(\n Object.entries(cliEnvRaw as Record<string, unknown>).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string',\n ),\n )\n : {};\n\n // ── Locate the config file ──\n const explicitPath = options.configPath ?? env['FRONTMCP_CONFIG'];\n let config: FrontMcpConfigParsed | undefined;\n let configPath: string | undefined;\n let configDir: string | undefined;\n\n if (explicitPath) {\n // Normalize to an absolute path so callers always see canonical metadata\n // regardless of how the caller-supplied path was spelt (relative, absolute,\n // or env-var-derived). `configDir` mirrors the auto-discovery branch.\n configPath = isAbsolute(explicitPath) ? explicitPath : pathResolve(options.cwd, explicitPath);\n configDir = dirname(configPath);\n try {\n // Soft variant: legacy exec-only configs (top-level `cli` / `sea` /\n // `esbuild`, no `deployments`) resolve to `config: undefined` so\n // `loadExecConfig` can still pick them up — same behaviour as the\n // auto-discovery branch below, per the file-header contract. Real\n // parse/schema failures still propagate as a hard error.\n config = await tryLoadFrontMcpConfigFromFile(configPath);\n } catch (err) {\n throw new Error(`Failed to load config from \"${explicitPath}\": ${(err as Error).message}`);\n }\n } else {\n configDir = await findConfigDir(options.cwd);\n if (configDir) {\n try {\n config = await tryLoadFrontMcpConfig(configDir);\n } catch (err) {\n // Surface schema/load errors — silent fallback was the corruption\n // mode #365 worked to eliminate. The legacy-shape branch inside\n // `tryLoadFrontMcpConfig` already returns `undefined` for old\n // exec-only configs, so anything reaching this catch is a real\n // parse failure that the caller needs to see.\n throw new Error(`Failed to load frontmcp.config in ${configDir}: ${(err as Error).message}`);\n }\n }\n }\n\n // ── Compose effective env ──\n const overlay = config?.env;\n const modeKey = modeToEnvKey(options.mode);\n const fromShared = overlay?.shared ?? {};\n const fromMode = (modeKey && overlay?.[modeKey]) ?? {};\n // Start from `process.env` so OS / CI / shell env still apply, then layer\n // shared + mode overlays + CLI-supplied env (later wins).\n const effectiveEnv: Record<string, string> = {};\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') effectiveEnv[key] = value;\n }\n Object.assign(effectiveEnv, fromShared, fromMode, cliEnv);\n\n return { config, configDir, configPath, effectiveEnv };\n}\n"]}
|