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.
Files changed (108) hide show
  1. package/package.json +4 -4
  2. package/src/commands/build/exec/bin-meta.d.ts +49 -0
  3. package/src/commands/build/exec/bin-meta.js +68 -0
  4. package/src/commands/build/exec/bin-meta.js.map +1 -0
  5. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
  6. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
  7. package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
  8. package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
  9. package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
  10. package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
  11. package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
  12. package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
  13. package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
  14. package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
  15. package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
  16. package/src/commands/build/exec/index.js +26 -0
  17. package/src/commands/build/exec/index.js.map +1 -1
  18. package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
  19. package/src/commands/dev/bridge/child-supervisor.js +228 -0
  20. package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
  21. package/src/commands/dev/bridge/errors.d.ts +23 -0
  22. package/src/commands/dev/bridge/errors.js +34 -0
  23. package/src/commands/dev/bridge/errors.js.map +1 -0
  24. package/src/commands/dev/bridge/index.d.ts +30 -0
  25. package/src/commands/dev/bridge/index.js +220 -0
  26. package/src/commands/dev/bridge/index.js.map +1 -0
  27. package/src/commands/dev/bridge/log.d.ts +29 -0
  28. package/src/commands/dev/bridge/log.js +82 -0
  29. package/src/commands/dev/bridge/log.js.map +1 -0
  30. package/src/commands/dev/bridge/state-machine.d.ts +56 -0
  31. package/src/commands/dev/bridge/state-machine.js +245 -0
  32. package/src/commands/dev/bridge/state-machine.js.map +1 -0
  33. package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
  34. package/src/commands/dev/bridge/stdio-framer.js +128 -0
  35. package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
  36. package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
  37. package/src/commands/dev/bridge/upstream-client.js +159 -0
  38. package/src/commands/dev/bridge/upstream-client.js.map +1 -0
  39. package/src/commands/dev/bridge/watcher.d.ts +30 -0
  40. package/src/commands/dev/bridge/watcher.js +87 -0
  41. package/src/commands/dev/bridge/watcher.js.map +1 -0
  42. package/src/commands/dev/dev.d.ts +18 -1
  43. package/src/commands/dev/dev.js +134 -14
  44. package/src/commands/dev/dev.js.map +1 -1
  45. package/src/commands/dev/inspector.d.ts +13 -1
  46. package/src/commands/dev/inspector.js +77 -3
  47. package/src/commands/dev/inspector.js.map +1 -1
  48. package/src/commands/dev/port.d.ts +23 -0
  49. package/src/commands/dev/port.js +87 -0
  50. package/src/commands/dev/port.js.map +1 -0
  51. package/src/commands/dev/register.d.ts +1 -1
  52. package/src/commands/dev/register.js +28 -4
  53. package/src/commands/dev/register.js.map +1 -1
  54. package/src/commands/dev/test.d.ts +26 -1
  55. package/src/commands/dev/test.js +181 -64
  56. package/src/commands/dev/test.js.map +1 -1
  57. package/src/commands/eject/mcp-client.d.ts +25 -0
  58. package/src/commands/eject/mcp-client.js +74 -0
  59. package/src/commands/eject/mcp-client.js.map +1 -0
  60. package/src/commands/eject/register.d.ts +9 -0
  61. package/src/commands/eject/register.js +56 -0
  62. package/src/commands/eject/register.js.map +1 -0
  63. package/src/commands/install/install-claude-plugin.d.ts +13 -0
  64. package/src/commands/install/install-claude-plugin.js +327 -0
  65. package/src/commands/install/install-claude-plugin.js.map +1 -0
  66. package/src/commands/install/register.d.ts +16 -0
  67. package/src/commands/install/register.js +70 -0
  68. package/src/commands/install/register.js.map +1 -0
  69. package/src/commands/scaffold/create.js +44 -0
  70. package/src/commands/scaffold/create.js.map +1 -1
  71. package/src/commands/skills/from-entry.d.ts +31 -0
  72. package/src/commands/skills/from-entry.js +68 -0
  73. package/src/commands/skills/from-entry.js.map +1 -0
  74. package/src/commands/skills/install.d.ts +12 -0
  75. package/src/commands/skills/install.js +173 -8
  76. package/src/commands/skills/install.js.map +1 -1
  77. package/src/commands/skills/register.js +7 -3
  78. package/src/commands/skills/register.js.map +1 -1
  79. package/src/config/frontmcp-config.loader.d.ts +28 -0
  80. package/src/config/frontmcp-config.loader.js +146 -67
  81. package/src/config/frontmcp-config.loader.js.map +1 -1
  82. package/src/config/frontmcp-config.resolve.d.ts +67 -0
  83. package/src/config/frontmcp-config.resolve.js +118 -0
  84. package/src/config/frontmcp-config.resolve.js.map +1 -0
  85. package/src/config/frontmcp-config.schema.d.ts +207 -0
  86. package/src/config/frontmcp-config.schema.js +217 -1
  87. package/src/config/frontmcp-config.schema.js.map +1 -1
  88. package/src/config/frontmcp-config.types.d.ts +133 -0
  89. package/src/config/frontmcp-config.types.js.map +1 -1
  90. package/src/config/index.d.ts +2 -1
  91. package/src/config/index.js +3 -1
  92. package/src/config/index.js.map +1 -1
  93. package/src/core/args.d.ts +13 -0
  94. package/src/core/args.js.map +1 -1
  95. package/src/core/bridge.js +39 -0
  96. package/src/core/bridge.js.map +1 -1
  97. package/src/core/cli.d.ts +0 -6
  98. package/src/core/cli.js +23 -3
  99. package/src/core/cli.js.map +1 -1
  100. package/src/core/help.d.ts +1 -1
  101. package/src/core/help.js +27 -6
  102. package/src/core/help.js.map +1 -1
  103. package/src/core/program.d.ts +1 -1
  104. package/src/core/program.js +56 -12
  105. package/src/core/program.js.map +1 -1
  106. package/src/core/project-commands.d.ts +44 -0
  107. package/src/core/project-commands.js +216 -0
  108. 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 = path.join(cwd, filename);
98
- if (!fs.existsSync(configPath))
165
+ const configPath = (0, utils_1.pathJoin)(cwd, filename);
166
+ if (!(await (0, utils_1.fileExists)(configPath)))
99
167
  continue;
100
- if (filename.endsWith('.json')) {
101
- const content = fs.readFileSync(configPath, 'utf-8');
102
- return JSON.parse(content);
103
- }
104
- if (filename.endsWith('.ts')) {
105
- // #365 — Loading `.ts` under `"type": "commonjs"` (the default) is a
106
- // minefield across Node versions:
107
- // - Node 20: `require()` throws on TS syntax, `await import()` errors
108
- // with "Make sure to set type: module".
109
- // - Node 22+: `require(esm)` may succeed but return partial data, OR
110
- // emit a warning on `await import()` even when the load succeeds.
111
- // - Node 24: type-stripping may swallow `import { x } from ...`
112
- // statements, returning `{}` instead of the user's exports — the
113
- // 1.1.2-beta.1 silent-defaults regression.
114
- // Round 3: under CJS, ALWAYS transpile via esbuild. It's the only path
115
- // that produces a deterministic, fully-typed result. ESM projects can
116
- // still use Node's runtime loaders since they're well-behaved there.
117
- const isCjsProject = await isCommonJsProject(cwd);
118
- if (isCjsProject) {
119
- try {
120
- return await loadTsConfigViaEsbuild(configPath);
121
- }
122
- catch (esbuildErr) {
123
- throw new Error(`Failed to load ${filename} via esbuild.\n` +
124
- ` ${esbuildErr.message}\n` +
125
- `Hint: ensure the file exports a default config (e.g., ` +
126
- `\`export default defineConfig({...})\`) and that all imports resolve.`);
127
- }
128
- }
129
- // ESM project ("type": "module"): try Node's loaders first (faster,
130
- // no transpile cost), fall back to esbuild on failure.
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
- ` require() error: ${requireErr?.message ?? '(skipped)'}\n` +
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
- // JS/MJS/CJS
158
- if (filename.endsWith('.mjs')) {
159
- const mod = await import(configPath);
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
- const mod = require(configPath);
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
- // Fallback: derive from package.json
166
- return deriveFromPackageJson(cwd);
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 = path.join(cwd, 'package.json');
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(path.dirname(configPath));
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 = path.join(cwd, 'package.json');
239
- if (!fs.existsSync(pkgPath)) {
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(fs.readFileSync(pkgPath, 'utf-8'));
321
+ const pkg = JSON.parse(await (0, utils_1.readFile)(pkgPath));
243
322
  return {
244
- name: pkg.name?.replace(/^@[^/]+\//, '') || path.basename(cwd),
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"]}