frontmcp 1.2.1 → 1.4.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/README.md +38 -29
- 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/build/exec/runner-script.js +16 -4
- package/src/commands/build/exec/runner-script.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 +34 -1
- package/src/commands/dev/dev.js +168 -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 +52 -8
- 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
- package/src/core/tsconfig.d.ts +20 -0
- package/src/core/tsconfig.js +41 -2
- package/src/core/tsconfig.js.map +1 -1
package/src/commands/dev/test.js
CHANGED
|
@@ -1,41 +1,115 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findUserJestConfig = findUserJestConfig;
|
|
4
|
+
exports.buildJestArgs = buildJestArgs;
|
|
5
|
+
exports.generateJestConfig = generateJestConfig;
|
|
3
6
|
exports.runTest = runTest;
|
|
4
7
|
const tslib_1 = require("tslib");
|
|
5
|
-
const path = tslib_1.__importStar(require("path"));
|
|
6
|
-
const os = tslib_1.__importStar(require("os"));
|
|
7
8
|
const child_process_1 = require("child_process");
|
|
8
|
-
const
|
|
9
|
+
const os = tslib_1.__importStar(require("os"));
|
|
10
|
+
const path = tslib_1.__importStar(require("path"));
|
|
9
11
|
const utils_1 = require("@frontmcp/utils");
|
|
10
|
-
const
|
|
12
|
+
const config_1 = require("../../config");
|
|
13
|
+
const colors_1 = require("../../core/colors");
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Filenames in cwd that, when present, cause `frontmcp test` to delegate to
|
|
16
|
+
* the user's own Jest config instead of injecting one. Order matters — Jest's
|
|
17
|
+
* own resolution is `ts > js > mjs > cjs > json`.
|
|
18
|
+
*/
|
|
19
|
+
const USER_JEST_CONFIG_FILES = [
|
|
20
|
+
'jest.config.ts',
|
|
21
|
+
'jest.config.js',
|
|
22
|
+
'jest.config.mjs',
|
|
23
|
+
'jest.config.cjs',
|
|
24
|
+
'jest.config.json',
|
|
25
|
+
];
|
|
26
|
+
async function findUserJestConfig(cwd) {
|
|
27
|
+
for (const name of USER_JEST_CONFIG_FILES) {
|
|
28
|
+
const p = path.join(cwd, name);
|
|
29
|
+
if (await (0, utils_1.fileExists)(p))
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the args passed to `npx jest`. Extracted from `runTest` so the args
|
|
36
|
+
* matrix can be unit-tested without spawning a subprocess.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
function buildJestArgs(configPath, opts, positionalPatterns = []) {
|
|
41
|
+
const args = ['jest', '--config', configPath];
|
|
42
|
+
if (opts.runInBand)
|
|
43
|
+
args.push('--runInBand');
|
|
44
|
+
if (opts.watch)
|
|
45
|
+
args.push('--watch');
|
|
46
|
+
if (opts.verbose)
|
|
47
|
+
args.push('--verbose');
|
|
48
|
+
if (opts.coverage)
|
|
49
|
+
args.push('--coverage');
|
|
50
|
+
if (positionalPatterns.length > 0)
|
|
51
|
+
args.push(...positionalPatterns);
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate Jest configuration programmatically.
|
|
56
|
+
*
|
|
57
|
+
* Issue #402: the original config (a) only ran `e2e/**` and `**\/*.e2e.ts`,
|
|
58
|
+
* missing the `*.spec.ts(x)` colocated unit tests mandated by CLAUDE.md, and
|
|
59
|
+
* (b) only transformed `.ts`/`.js` with a `typescript` parser, so any `.tsx`
|
|
60
|
+
* file failed both the regex AND SWC's parser. This generator now:
|
|
61
|
+
* - matches both colocated unit specs and e2e specs (`.ts` + `.tsx`),
|
|
62
|
+
* - transforms `.tsx`/`.jsx` files with `tsx: true` and the automatic JSX
|
|
63
|
+
* runtime so React components are usable in tests,
|
|
64
|
+
* - exposes the helper for unit testing.
|
|
14
65
|
*/
|
|
15
|
-
function generateJestConfig(cwd, opts) {
|
|
16
|
-
|
|
66
|
+
function generateJestConfig(cwd, opts, testDefaults) {
|
|
67
|
+
// Issue #400 — config defaults apply when CLI flags are absent.
|
|
68
|
+
const testTimeout = opts.timeout ?? testDefaults?.timeoutMs ?? 60000;
|
|
17
69
|
return {
|
|
18
70
|
// Use Node.js environment for E2E tests
|
|
19
71
|
testEnvironment: 'node',
|
|
20
72
|
// Root directory for tests
|
|
21
73
|
rootDir: cwd,
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
//
|
|
74
|
+
// Match both colocated unit specs (`src/**/*.spec.ts(x)`,
|
|
75
|
+
// `**/__tests__/**/*.spec.ts(x)`) AND e2e specs. The repo convention per
|
|
76
|
+
// CLAUDE.md is `.spec.ts` (NOT `.test.ts`) for unit tests; `.e2e.spec.ts`
|
|
77
|
+
// for e2e; `.pw.spec.ts` for Playwright; `.perf.spec.ts` for perf.
|
|
78
|
+
//
|
|
79
|
+
// CodeRabbit PR #425: removed `e2e/**/*.e2e.ts(x)` and `**/*.e2e.ts(x)`
|
|
80
|
+
// patterns — the convention is strictly `.e2e.spec.ts(x)`, so matching
|
|
81
|
+
// `.e2e.ts` would let stragglers that violate the convention slip
|
|
82
|
+
// through discovery.
|
|
83
|
+
testMatch: testDefaults?.testMatch ?? [
|
|
84
|
+
'<rootDir>/src/**/*.spec.ts',
|
|
85
|
+
'<rootDir>/src/**/*.spec.tsx',
|
|
86
|
+
'<rootDir>/**/__tests__/**/*.spec.ts',
|
|
87
|
+
'<rootDir>/**/__tests__/**/*.spec.tsx',
|
|
88
|
+
'<rootDir>/e2e/**/*.e2e.spec.ts',
|
|
89
|
+
'<rootDir>/e2e/**/*.e2e.spec.tsx',
|
|
90
|
+
],
|
|
91
|
+
// Transform TypeScript files using @swc/jest for speed. Accepts both
|
|
92
|
+
// `.ts/.js` and `.tsx/.jsx`; the SWC parser is set up with `tsx: true`
|
|
93
|
+
// and the automatic JSX runtime so React components work without
|
|
94
|
+
// additional setup.
|
|
25
95
|
transform: {
|
|
26
|
-
'^.+\\.[tj]
|
|
96
|
+
'^.+\\.[tj]sx?$': [
|
|
27
97
|
'@swc/jest',
|
|
28
98
|
{
|
|
29
99
|
jsc: {
|
|
30
100
|
target: 'es2022',
|
|
31
101
|
parser: {
|
|
32
102
|
syntax: 'typescript',
|
|
103
|
+
tsx: true,
|
|
33
104
|
decorators: true,
|
|
34
105
|
dynamicImport: true,
|
|
35
106
|
},
|
|
36
107
|
transform: {
|
|
37
108
|
decoratorMetadata: true,
|
|
38
109
|
legacyDecorator: true,
|
|
110
|
+
react: {
|
|
111
|
+
runtime: 'automatic',
|
|
112
|
+
},
|
|
39
113
|
},
|
|
40
114
|
keepClassNames: true,
|
|
41
115
|
externalHelpers: true,
|
|
@@ -59,14 +133,19 @@ function generateJestConfig(cwd, opts) {
|
|
|
59
133
|
transformIgnorePatterns: ['node_modules/(?!(jose)/)'],
|
|
60
134
|
// Ignore patterns
|
|
61
135
|
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
|
62
|
-
// Coverage settings
|
|
63
|
-
collectCoverage: opts.coverage ?? false,
|
|
136
|
+
// Coverage settings (issue #400: CLI > config > false)
|
|
137
|
+
collectCoverage: opts.coverage ?? testDefaults?.coverage ?? false,
|
|
64
138
|
// Coverage configuration when enabled
|
|
65
|
-
...(opts.coverage
|
|
139
|
+
...((opts.coverage ?? testDefaults?.coverage)
|
|
66
140
|
? {
|
|
67
141
|
coverageDirectory: '<rootDir>/coverage',
|
|
68
142
|
coverageReporters: ['text', 'lcov', 'json'],
|
|
69
|
-
collectCoverageFrom: [
|
|
143
|
+
collectCoverageFrom: [
|
|
144
|
+
'<rootDir>/src/**/*.ts',
|
|
145
|
+
'<rootDir>/src/**/*.tsx',
|
|
146
|
+
'!<rootDir>/src/**/*.spec.ts',
|
|
147
|
+
'!<rootDir>/src/**/*.spec.tsx',
|
|
148
|
+
],
|
|
70
149
|
}
|
|
71
150
|
: {}),
|
|
72
151
|
// Verbose output
|
|
@@ -85,58 +164,92 @@ function generateJestConfig(cwd, opts) {
|
|
|
85
164
|
*/
|
|
86
165
|
async function runTest(opts) {
|
|
87
166
|
const cwd = process.cwd();
|
|
88
|
-
//
|
|
167
|
+
// Issue #400 — resolve frontmcp.config so `test.timeoutMs` /
|
|
168
|
+
// `test.runInBand` / `test.coverage` / `test.testMatch` apply when the
|
|
169
|
+
// user didn't pass the equivalent CLI flag.
|
|
170
|
+
const resolved = await (0, config_1.resolveConfig)({
|
|
171
|
+
cwd,
|
|
172
|
+
mode: 'test',
|
|
173
|
+
configPath: typeof opts.config === 'string' ? opts.config : undefined,
|
|
174
|
+
});
|
|
175
|
+
const testDefaults = resolved.config?.test;
|
|
176
|
+
const mergedOpts = {
|
|
177
|
+
...opts,
|
|
178
|
+
runInBand: opts.runInBand ?? testDefaults?.runInBand,
|
|
179
|
+
coverage: opts.coverage ?? testDefaults?.coverage,
|
|
180
|
+
timeout: opts.timeout ?? testDefaults?.timeoutMs,
|
|
181
|
+
};
|
|
182
|
+
// Issue #402: honor an existing `jest.config.{ts,js,mjs,cjs,json}` in cwd
|
|
183
|
+
// by delegating to it instead of injecting our own config. Users who need
|
|
184
|
+
// bespoke behaviour (custom transforms, projects, setup files, …) shouldn't
|
|
185
|
+
// be forced to overlay it on top of our injection.
|
|
186
|
+
const userConfig = await findUserJestConfig(cwd);
|
|
187
|
+
// Issue #402: previously this command HARD-REQUIRED a `./e2e/` directory
|
|
188
|
+
// and exited 1 otherwise — locking out projects that only have colocated
|
|
189
|
+
// unit specs (the CLAUDE.md convention). The new testMatch covers both;
|
|
190
|
+
// we only refuse to run when there's literally nowhere to look.
|
|
191
|
+
//
|
|
192
|
+
// CodeRabbit on PR #425: also accept `__tests__/`-only projects, since
|
|
193
|
+
// `generateJestConfig` discovers `**/__tests__/**/*.spec.ts(x)` too. The
|
|
194
|
+
// help text below matches the actual default discovery rules.
|
|
89
195
|
const e2eDir = path.join(cwd, 'e2e');
|
|
196
|
+
const srcDir = path.join(cwd, 'src');
|
|
197
|
+
const testsDir = path.join(cwd, '__tests__');
|
|
90
198
|
const hasE2EDir = await (0, utils_1.fileExists)(e2eDir);
|
|
91
|
-
|
|
92
|
-
|
|
199
|
+
const hasSrcDir = await (0, utils_1.fileExists)(srcDir);
|
|
200
|
+
const hasTestsDir = await (0, utils_1.fileExists)(testsDir);
|
|
201
|
+
if (!userConfig && !hasE2EDir && !hasSrcDir && !hasTestsDir) {
|
|
202
|
+
console.error((0, colors_1.c)('red', 'No test sources found.'));
|
|
93
203
|
console.error('');
|
|
94
|
-
console.error('Expected
|
|
95
|
-
console.error('
|
|
96
|
-
console.error('
|
|
97
|
-
console.error('
|
|
204
|
+
console.error('Expected one of:');
|
|
205
|
+
console.error(' • a jest.config.{ts,js,mjs,cjs,json} in the current directory');
|
|
206
|
+
console.error(' • a ./src/ directory with colocated *.spec.ts(x) files');
|
|
207
|
+
console.error(' • a ./__tests__/ directory with *.spec.ts(x) files');
|
|
208
|
+
console.error(' • a ./e2e/ directory with *.e2e.spec.ts(x) files');
|
|
98
209
|
console.error('');
|
|
99
|
-
console.error('Create
|
|
210
|
+
console.error('Create one of those, then run:');
|
|
100
211
|
console.error(' frontmcp test');
|
|
101
212
|
process.exit(1);
|
|
102
213
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (opts.runInBand) {
|
|
112
|
-
jestArgs.push('--runInBand');
|
|
214
|
+
// Build Jest arguments. With a user config, we delegate; otherwise we
|
|
215
|
+
// write our generated config to a temp file and point Jest at it.
|
|
216
|
+
let configPath;
|
|
217
|
+
if (!userConfig) {
|
|
218
|
+
const config = generateJestConfig(cwd, mergedOpts, testDefaults);
|
|
219
|
+
const tempDir = os.tmpdir();
|
|
220
|
+
configPath = path.join(tempDir, `frontmcp-jest-config-${Date.now()}.json`);
|
|
221
|
+
await (0, utils_1.writeFile)(configPath, JSON.stringify(config, null, 2));
|
|
113
222
|
}
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
223
|
+
// Explicit narrowing avoids the non-null assertion on userConfig. By
|
|
224
|
+
// construction, the branch above guarantees that exactly one of
|
|
225
|
+
// `configPath` or `userConfig` is set when we get here — but TS can't
|
|
226
|
+
// see through that flow, so an explicit guard keeps the type checker
|
|
227
|
+
// honest without `!`.
|
|
228
|
+
const selectedConfig = configPath ?? userConfig;
|
|
229
|
+
if (!selectedConfig) {
|
|
230
|
+
// Defensive: should be unreachable thanks to the no-sources check above.
|
|
231
|
+
throw new Error('Internal error: no Jest config selected.');
|
|
117
232
|
}
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
233
|
+
// Positional test patterns: everything after the `test` command itself.
|
|
234
|
+
const testPatterns = opts._.slice(1);
|
|
235
|
+
const jestArgs = buildJestArgs(selectedConfig, mergedOpts, testPatterns);
|
|
236
|
+
console.log(`${(0, colors_1.c)('cyan', '[test]')} running tests in ${path.relative(process.cwd(), cwd) || '.'}`);
|
|
237
|
+
if (resolved.configPath || resolved.configDir) {
|
|
238
|
+
console.log(`${(0, colors_1.c)('gray', '[test]')} config: ${resolved.configPath ?? resolved.configDir}`);
|
|
121
239
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
jestArgs.push('--coverage');
|
|
240
|
+
if (userConfig) {
|
|
241
|
+
console.log(`${(0, colors_1.c)('gray', '[test]')} using user Jest config: ${path.relative(cwd, userConfig)}`);
|
|
125
242
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (testPatterns.length > 0) {
|
|
129
|
-
jestArgs.push(...testPatterns);
|
|
243
|
+
else {
|
|
244
|
+
console.log(`${(0, colors_1.c)('gray', '[test]')} using auto-injected Jest configuration`);
|
|
130
245
|
}
|
|
131
|
-
|
|
132
|
-
console.log(`${(0, colors_1.c)('gray', '[test]')} using auto-injected Jest configuration`);
|
|
133
|
-
if (opts.runInBand) {
|
|
246
|
+
if (mergedOpts.runInBand) {
|
|
134
247
|
console.log(`${(0, colors_1.c)('gray', '[test]')} running tests sequentially (--runInBand)`);
|
|
135
248
|
}
|
|
136
|
-
if (
|
|
249
|
+
if (mergedOpts.watch) {
|
|
137
250
|
console.log(`${(0, colors_1.c)('gray', '[test]')} watch mode enabled`);
|
|
138
251
|
}
|
|
139
|
-
if (
|
|
252
|
+
if (mergedOpts.coverage) {
|
|
140
253
|
console.log(`${(0, colors_1.c)('gray', '[test]')} coverage collection enabled`);
|
|
141
254
|
}
|
|
142
255
|
console.log(`${(0, colors_1.c)('gray', 'hint:')} press Ctrl+C to stop\n`);
|
|
@@ -157,12 +270,14 @@ async function runTest(opts) {
|
|
|
157
270
|
catch {
|
|
158
271
|
// ignore
|
|
159
272
|
}
|
|
160
|
-
// Clean up temp config file
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
273
|
+
// Clean up temp config file (only when we generated one — never the user's).
|
|
274
|
+
if (configPath) {
|
|
275
|
+
try {
|
|
276
|
+
await (0, utils_1.unlink)(configPath);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// ignore
|
|
280
|
+
}
|
|
166
281
|
}
|
|
167
282
|
};
|
|
168
283
|
process.on('SIGINT', () => {
|
|
@@ -187,12 +302,14 @@ async function runTest(opts) {
|
|
|
187
302
|
});
|
|
188
303
|
}
|
|
189
304
|
finally {
|
|
190
|
-
// Clean up temp config file
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
305
|
+
// Clean up temp config file (only when we generated one — never the user's).
|
|
306
|
+
if (configPath) {
|
|
307
|
+
try {
|
|
308
|
+
await (0, utils_1.unlink)(configPath);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// ignore
|
|
312
|
+
}
|
|
196
313
|
}
|
|
197
314
|
}
|
|
198
315
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.js","sourceRoot":"","sources":["../../../../src/commands/dev/test.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport * as os from 'os';\nimport { spawn, ChildProcess } from 'child_process';\nimport { ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\nimport { fileExists } from '@frontmcp/utils';\nimport { fsp } from '../../shared/fs';\n\n/**\n * Generate Jest configuration programmatically for E2E tests.\n * This eliminates the need for projects to have their own jest.e2e.config.ts.\n */\nfunction generateJestConfig(cwd: string, opts: ParsedArgs): object {\n const testTimeout = opts.timeout ?? 60000;\n\n return {\n // Use Node.js environment for E2E tests\n testEnvironment: 'node',\n\n // Root directory for tests\n rootDir: cwd,\n\n // Test file patterns for E2E tests\n testMatch: ['<rootDir>/e2e/**/*.e2e.ts', '<rootDir>/e2e/**/*.e2e.spec.ts', '<rootDir>/**/*.e2e.ts'],\n\n // Transform TypeScript files using @swc/jest for speed\n transform: {\n '^.+\\\\.[tj]s$': [\n '@swc/jest',\n {\n jsc: {\n target: 'es2022',\n parser: {\n syntax: 'typescript',\n decorators: true,\n dynamicImport: true,\n },\n transform: {\n decoratorMetadata: true,\n legacyDecorator: true,\n },\n keepClassNames: true,\n externalHelpers: true,\n loose: true,\n },\n module: {\n type: 'es6',\n },\n sourceMaps: true,\n swcrc: false,\n },\n ],\n },\n\n // File extensions to consider\n moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],\n\n // Test timeout\n testTimeout,\n\n // Setup files that run after Jest is initialized\n setupFilesAfterEnv: ['@frontmcp/testing/setup'],\n\n // Transform packages that use ESM\n transformIgnorePatterns: ['node_modules/(?!(jose)/)'],\n\n // Ignore patterns\n testPathIgnorePatterns: ['/node_modules/', '/dist/'],\n\n // Coverage settings\n collectCoverage: opts.coverage ?? false,\n\n // Coverage configuration when enabled\n ...(opts.coverage\n ? {\n coverageDirectory: '<rootDir>/coverage',\n coverageReporters: ['text', 'lcov', 'json'],\n collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!<rootDir>/src/**/*.spec.ts'],\n }\n : {}),\n\n // Verbose output\n verbose: opts.verbose ?? true,\n };\n}\n\n/**\n * Run E2E tests using Jest with auto-injected configuration.\n *\n * Usage:\n * frontmcp test # Run E2E tests in current directory\n * frontmcp test --runInBand # Run tests sequentially (recommended for E2E)\n * frontmcp test --watch # Run tests in watch mode\n * frontmcp test --verbose # Show verbose output\n * frontmcp test --timeout 60000 # Set test timeout (default: 60000ms)\n */\nexport async function runTest(opts: ParsedArgs): Promise<void> {\n const cwd = process.cwd();\n\n // Check for e2e directory\n const e2eDir = path.join(cwd, 'e2e');\n const hasE2EDir = await fileExists(e2eDir);\n\n if (!hasE2EDir) {\n console.error(c('red', 'No e2e directory found.'));\n console.error('');\n console.error('Expected structure:');\n console.error(' ./e2e/');\n console.error(' ├── your-test.e2e.ts');\n console.error(' └── another-test.e2e.spec.ts');\n console.error('');\n console.error('Create an e2e directory with test files, then run:');\n console.error(' frontmcp test');\n process.exit(1);\n }\n\n // Generate Jest config and write to temp file\n const config = generateJestConfig(cwd, opts);\n const tempDir = os.tmpdir();\n const configPath = path.join(tempDir, `frontmcp-jest-config-${Date.now()}.json`);\n await fsp.writeFile(configPath, JSON.stringify(config, null, 2));\n\n // Build Jest arguments\n const jestArgs: string[] = ['jest', '--config', configPath];\n\n // Add --runInBand for sequential execution (recommended for E2E tests)\n if (opts.runInBand) {\n jestArgs.push('--runInBand');\n }\n\n // Add watch mode\n if (opts.watch) {\n jestArgs.push('--watch');\n }\n\n // Add verbose flag\n if (opts.verbose) {\n jestArgs.push('--verbose');\n }\n\n // Add coverage flag\n if (opts.coverage) {\n jestArgs.push('--coverage');\n }\n\n // Add any additional positional args (e.g., test file patterns)\n const testPatterns = opts._.slice(1); // Skip 'test' command itself\n if (testPatterns.length > 0) {\n jestArgs.push(...testPatterns);\n }\n\n console.log(`${c('cyan', '[test]')} running E2E tests in ${path.relative(process.cwd(), cwd) || '.'}`);\n console.log(`${c('gray', '[test]')} using auto-injected Jest configuration`);\n\n if (opts.runInBand) {\n console.log(`${c('gray', '[test]')} running tests sequentially (--runInBand)`);\n }\n\n if (opts.watch) {\n console.log(`${c('gray', '[test]')} watch mode enabled`);\n }\n\n if (opts.coverage) {\n console.log(`${c('gray', '[test]')} coverage collection enabled`);\n }\n\n console.log(`${c('gray', 'hint:')} press Ctrl+C to stop\\n`);\n\n // Run Jest directly via node_modules/.bin or npx without shell\n // Using shell: false with explicit args array avoids escaping issues\n const jest = spawn('npx', jestArgs, {\n stdio: 'inherit',\n shell: false,\n cwd,\n });\n\n // Handle cleanup\n const cleanup = async (proc?: ChildProcess) => {\n try {\n if (proc) {\n proc.kill('SIGINT');\n }\n } catch {\n // ignore\n }\n // Clean up temp config file\n try {\n await fsp.unlink(configPath);\n } catch {\n // ignore\n }\n };\n\n process.on('SIGINT', () => {\n cleanup(jest).finally(() => {\n process.exit(0);\n });\n });\n\n // Wait for Jest to complete\n try {\n await new Promise<void>((resolve, reject) => {\n jest.on('close', (code) => {\n if (code === 0) {\n resolve();\n } else {\n reject(new Error(`Jest exited with code ${code}`));\n }\n });\n jest.on('error', (err) => {\n reject(err);\n });\n });\n } finally {\n // Clean up temp config file\n try {\n await fsp.unlink(configPath);\n } catch {\n // ignore\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../../../../src/commands/dev/test.ts"],"names":[],"mappings":";;AAuBA,gDAOC;AAQD,sCAQC;AAcD,gDAwGC;AAYD,0BA+JC;;AA/UD,iDAAyD;AACzD,+CAAyB;AACzB,mDAA6B;AAE7B,2CAAgE;AAEhE,yCAA6C;AAE7C,8CAAsC;AAEtC;;;;GAIG;AACH,MAAM,sBAAsB,GAAG;IAC7B,gBAAgB;IAChB,gBAAgB;IAChB,iBAAiB;IACjB,iBAAiB;IACjB,kBAAkB;CACnB,CAAC;AAEK,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,KAAK,MAAM,IAAI,IAAI,sBAAsB,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAE/B,IAAI,MAAM,IAAA,kBAAU,EAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,UAAkB,EAAE,IAAgB,EAAE,qBAA+B,EAAE;IACnG,MAAM,IAAI,GAAa,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IACxD,IAAI,IAAI,CAAC,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrC,IAAI,IAAI,CAAC,OAAO;QAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACzC,IAAI,IAAI,CAAC,QAAQ;QAAE,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,kBAAkB,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,kBAAkB,CAChC,GAAW,EACX,IAAgB,EAChB,YAA+E;IAE/E,gEAAgE;IAChE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,IAAI,YAAY,EAAE,SAAS,IAAI,KAAK,CAAC;IAErE,OAAO;QACL,wCAAwC;QACxC,eAAe,EAAE,MAAM;QAEvB,2BAA2B;QAC3B,OAAO,EAAE,GAAG;QAEZ,0DAA0D;QAC1D,yEAAyE;QACzE,0EAA0E;QAC1E,mEAAmE;QACnE,EAAE;QACF,wEAAwE;QACxE,uEAAuE;QACvE,kEAAkE;QAClE,qBAAqB;QACrB,SAAS,EAAE,YAAY,EAAE,SAAS,IAAI;YACpC,4BAA4B;YAC5B,6BAA6B;YAC7B,qCAAqC;YACrC,sCAAsC;YACtC,gCAAgC;YAChC,iCAAiC;SAClC;QAED,qEAAqE;QACrE,uEAAuE;QACvE,iEAAiE;QACjE,oBAAoB;QACpB,SAAS,EAAE;YACT,gBAAgB,EAAE;gBAChB,WAAW;gBACX;oBACE,GAAG,EAAE;wBACH,MAAM,EAAE,QAAQ;wBAChB,MAAM,EAAE;4BACN,MAAM,EAAE,YAAY;4BACpB,GAAG,EAAE,IAAI;4BACT,UAAU,EAAE,IAAI;4BAChB,aAAa,EAAE,IAAI;yBACpB;wBACD,SAAS,EAAE;4BACT,iBAAiB,EAAE,IAAI;4BACvB,eAAe,EAAE,IAAI;4BACrB,KAAK,EAAE;gCACL,OAAO,EAAE,WAAW;6BACrB;yBACF;wBACD,cAAc,EAAE,IAAI;wBACpB,eAAe,EAAE,IAAI;wBACrB,KAAK,EAAE,IAAI;qBACZ;oBACD,MAAM,EAAE;wBACN,IAAI,EAAE,KAAK;qBACZ;oBACD,UAAU,EAAE,IAAI;oBAChB,KAAK,EAAE,KAAK;iBACb;aACF;SACF;QAED,8BAA8B;QAC9B,oBAAoB,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;QAEhE,eAAe;QACf,WAAW;QAEX,iDAAiD;QACjD,kBAAkB,EAAE,CAAC,yBAAyB,CAAC;QAE/C,kCAAkC;QAClC,uBAAuB,EAAE,CAAC,0BAA0B,CAAC;QAErD,kBAAkB;QAClB,sBAAsB,EAAE,CAAC,gBAAgB,EAAE,QAAQ,CAAC;QAEpD,uDAAuD;QACvD,eAAe,EAAE,IAAI,CAAC,QAAQ,IAAI,YAAY,EAAE,QAAQ,IAAI,KAAK;QAEjE,sCAAsC;QACtC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,YAAY,EAAE,QAAQ,CAAC;YAC3C,CAAC,CAAC;gBACE,iBAAiB,EAAE,oBAAoB;gBACvC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;gBAC3C,mBAAmB,EAAE;oBACnB,uBAAuB;oBACvB,wBAAwB;oBACxB,6BAA6B;oBAC7B,8BAA8B;iBAC/B;aACF;YACH,CAAC,CAAC,EAAE,CAAC;QAEP,iBAAiB;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,OAAO,CAAC,IAAgB;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAE1B,6DAA6D;IAC7D,uEAAuE;IACvE,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,MAAM,IAAA,sBAAa,EAAC;QACnC,GAAG;QACH,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;KACtE,CAAC,CAAC;IACH,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IAC3C,MAAM,UAAU,GAAG;QACjB,GAAG,IAAI;QACP,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,YAAY,EAAE,SAAS;QACpD,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,YAAY,EAAE,QAAQ;QACjD,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,YAAY,EAAE,SAAS;KACnC,CAAC;IAEhB,0EAA0E;IAC1E,0EAA0E;IAC1E,4EAA4E;IAC5E,mDAAmD;IACnD,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAEjD,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,gEAAgE;IAChE,EAAE;IACF,uEAAuE;IACvE,yEAAyE;IACzE,8DAA8D;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,MAAM,IAAA,kBAAU,EAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAM,IAAA,kBAAU,EAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,MAAM,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IAE/C,IAAI,CAAC,UAAU,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,CAAC;QAC5D,OAAO,CAAC,KAAK,CAAC,IAAA,UAAC,EAAC,KAAK,EAAE,wBAAwB,CAAC,CAAC,CAAC;QAClD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QAClC,OAAO,CAAC,KAAK,CAAC,iEAAiE,CAAC,CAAC;QACjF,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;QAC1E,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;QACtE,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACpE,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAChD,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,sEAAsE;IACtE,kEAAkE;IAClE,IAAI,UAA8B,CAAC;IACnC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC;QAC5B,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,wBAAwB,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC3E,MAAM,IAAA,iBAAS,EAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,qEAAqE;IACrE,gEAAgE;IAChE,sEAAsE;IACtE,qEAAqE;IACrE,sBAAsB;IACtB,MAAM,cAAc,GAAG,UAAU,IAAI,UAAU,CAAC;IAChD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,yEAAyE;QACzE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,wEAAwE;IACxE,MAAM,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,aAAa,CAAC,cAAc,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;IAEzE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,qBAAqB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;IACnG,IAAI,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,YAAY,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,4BAA4B,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IAClG,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,yCAAyC,CAAC,CAAC;IAC/E,CAAC;IAED,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,2CAA2C,CAAC,CAAC;IACjF,CAAC;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,qBAAqB,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,QAAQ,CAAC,8BAA8B,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,GAAG,IAAA,UAAC,EAAC,MAAM,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAE5D,+DAA+D;IAC/D,qEAAqE;IACrE,MAAM,IAAI,GAAG,IAAA,qBAAK,EAAC,KAAK,EAAE,QAAQ,EAAE;QAClC,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,KAAK;QACZ,GAAG;KACJ,CAAC,CAAC;IAEH,iBAAiB;IACjB,MAAM,OAAO,GAAG,KAAK,EAAE,IAAmB,EAAE,EAAE;QAC5C,IAAI,CAAC;YACH,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,6EAA6E;QAC7E,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,IAAA,cAAM,EAAC,UAAU,CAAC,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;YACzB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,4BAA4B;IAC5B,IAAI,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gBACxB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,OAAO,EAAE,CAAC;gBACZ,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC,CAAC;gBACrD,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACvB,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,6EAA6E;QAC7E,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,IAAA,cAAM,EAAC,UAAU,CAAC,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import { spawn, type ChildProcess } from 'child_process';\nimport * as os from 'os';\nimport * as path from 'path';\n\nimport { fileExists, unlink, writeFile } from '@frontmcp/utils';\n\nimport { resolveConfig } from '../../config';\nimport { type ParsedArgs } from '../../core/args';\nimport { c } from '../../core/colors';\n\n/**\n * Filenames in cwd that, when present, cause `frontmcp test` to delegate to\n * the user's own Jest config instead of injecting one. Order matters — Jest's\n * own resolution is `ts > js > mjs > cjs > json`.\n */\nconst USER_JEST_CONFIG_FILES = [\n 'jest.config.ts',\n 'jest.config.js',\n 'jest.config.mjs',\n 'jest.config.cjs',\n 'jest.config.json',\n];\n\nexport async function findUserJestConfig(cwd: string): Promise<string | undefined> {\n for (const name of USER_JEST_CONFIG_FILES) {\n const p = path.join(cwd, name);\n\n if (await fileExists(p)) return p;\n }\n return undefined;\n}\n\n/**\n * Build the args passed to `npx jest`. Extracted from `runTest` so the args\n * matrix can be unit-tested without spawning a subprocess.\n *\n * @internal\n */\nexport function buildJestArgs(configPath: string, opts: ParsedArgs, positionalPatterns: string[] = []): string[] {\n const args: string[] = ['jest', '--config', configPath];\n if (opts.runInBand) args.push('--runInBand');\n if (opts.watch) args.push('--watch');\n if (opts.verbose) args.push('--verbose');\n if (opts.coverage) args.push('--coverage');\n if (positionalPatterns.length > 0) args.push(...positionalPatterns);\n return args;\n}\n\n/**\n * Generate Jest configuration programmatically.\n *\n * Issue #402: the original config (a) only ran `e2e/**` and `**\\/*.e2e.ts`,\n * missing the `*.spec.ts(x)` colocated unit tests mandated by CLAUDE.md, and\n * (b) only transformed `.ts`/`.js` with a `typescript` parser, so any `.tsx`\n * file failed both the regex AND SWC's parser. This generator now:\n * - matches both colocated unit specs and e2e specs (`.ts` + `.tsx`),\n * - transforms `.tsx`/`.jsx` files with `tsx: true` and the automatic JSX\n * runtime so React components are usable in tests,\n * - exposes the helper for unit testing.\n */\nexport function generateJestConfig(\n cwd: string,\n opts: ParsedArgs,\n testDefaults?: { timeoutMs?: number; testMatch?: string[]; coverage?: boolean },\n): object {\n // Issue #400 — config defaults apply when CLI flags are absent.\n const testTimeout = opts.timeout ?? testDefaults?.timeoutMs ?? 60000;\n\n return {\n // Use Node.js environment for E2E tests\n testEnvironment: 'node',\n\n // Root directory for tests\n rootDir: cwd,\n\n // Match both colocated unit specs (`src/**/*.spec.ts(x)`,\n // `**/__tests__/**/*.spec.ts(x)`) AND e2e specs. The repo convention per\n // CLAUDE.md is `.spec.ts` (NOT `.test.ts`) for unit tests; `.e2e.spec.ts`\n // for e2e; `.pw.spec.ts` for Playwright; `.perf.spec.ts` for perf.\n //\n // CodeRabbit PR #425: removed `e2e/**/*.e2e.ts(x)` and `**/*.e2e.ts(x)`\n // patterns — the convention is strictly `.e2e.spec.ts(x)`, so matching\n // `.e2e.ts` would let stragglers that violate the convention slip\n // through discovery.\n testMatch: testDefaults?.testMatch ?? [\n '<rootDir>/src/**/*.spec.ts',\n '<rootDir>/src/**/*.spec.tsx',\n '<rootDir>/**/__tests__/**/*.spec.ts',\n '<rootDir>/**/__tests__/**/*.spec.tsx',\n '<rootDir>/e2e/**/*.e2e.spec.ts',\n '<rootDir>/e2e/**/*.e2e.spec.tsx',\n ],\n\n // Transform TypeScript files using @swc/jest for speed. Accepts both\n // `.ts/.js` and `.tsx/.jsx`; the SWC parser is set up with `tsx: true`\n // and the automatic JSX runtime so React components work without\n // additional setup.\n transform: {\n '^.+\\\\.[tj]sx?$': [\n '@swc/jest',\n {\n jsc: {\n target: 'es2022',\n parser: {\n syntax: 'typescript',\n tsx: true,\n decorators: true,\n dynamicImport: true,\n },\n transform: {\n decoratorMetadata: true,\n legacyDecorator: true,\n react: {\n runtime: 'automatic',\n },\n },\n keepClassNames: true,\n externalHelpers: true,\n loose: true,\n },\n module: {\n type: 'es6',\n },\n sourceMaps: true,\n swcrc: false,\n },\n ],\n },\n\n // File extensions to consider\n moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],\n\n // Test timeout\n testTimeout,\n\n // Setup files that run after Jest is initialized\n setupFilesAfterEnv: ['@frontmcp/testing/setup'],\n\n // Transform packages that use ESM\n transformIgnorePatterns: ['node_modules/(?!(jose)/)'],\n\n // Ignore patterns\n testPathIgnorePatterns: ['/node_modules/', '/dist/'],\n\n // Coverage settings (issue #400: CLI > config > false)\n collectCoverage: opts.coverage ?? testDefaults?.coverage ?? false,\n\n // Coverage configuration when enabled\n ...((opts.coverage ?? testDefaults?.coverage)\n ? {\n coverageDirectory: '<rootDir>/coverage',\n coverageReporters: ['text', 'lcov', 'json'],\n collectCoverageFrom: [\n '<rootDir>/src/**/*.ts',\n '<rootDir>/src/**/*.tsx',\n '!<rootDir>/src/**/*.spec.ts',\n '!<rootDir>/src/**/*.spec.tsx',\n ],\n }\n : {}),\n\n // Verbose output\n verbose: opts.verbose ?? true,\n };\n}\n\n/**\n * Run E2E tests using Jest with auto-injected configuration.\n *\n * Usage:\n * frontmcp test # Run E2E tests in current directory\n * frontmcp test --runInBand # Run tests sequentially (recommended for E2E)\n * frontmcp test --watch # Run tests in watch mode\n * frontmcp test --verbose # Show verbose output\n * frontmcp test --timeout 60000 # Set test timeout (default: 60000ms)\n */\nexport async function runTest(opts: ParsedArgs): Promise<void> {\n const cwd = process.cwd();\n\n // Issue #400 — resolve frontmcp.config so `test.timeoutMs` /\n // `test.runInBand` / `test.coverage` / `test.testMatch` apply when the\n // user didn't pass the equivalent CLI flag.\n const resolved = await resolveConfig({\n cwd,\n mode: 'test',\n configPath: typeof opts.config === 'string' ? opts.config : undefined,\n });\n const testDefaults = resolved.config?.test;\n const mergedOpts = {\n ...opts,\n runInBand: opts.runInBand ?? testDefaults?.runInBand,\n coverage: opts.coverage ?? testDefaults?.coverage,\n timeout: opts.timeout ?? testDefaults?.timeoutMs,\n } as ParsedArgs;\n\n // Issue #402: honor an existing `jest.config.{ts,js,mjs,cjs,json}` in cwd\n // by delegating to it instead of injecting our own config. Users who need\n // bespoke behaviour (custom transforms, projects, setup files, …) shouldn't\n // be forced to overlay it on top of our injection.\n const userConfig = await findUserJestConfig(cwd);\n\n // Issue #402: previously this command HARD-REQUIRED a `./e2e/` directory\n // and exited 1 otherwise — locking out projects that only have colocated\n // unit specs (the CLAUDE.md convention). The new testMatch covers both;\n // we only refuse to run when there's literally nowhere to look.\n //\n // CodeRabbit on PR #425: also accept `__tests__/`-only projects, since\n // `generateJestConfig` discovers `**/__tests__/**/*.spec.ts(x)` too. The\n // help text below matches the actual default discovery rules.\n const e2eDir = path.join(cwd, 'e2e');\n const srcDir = path.join(cwd, 'src');\n const testsDir = path.join(cwd, '__tests__');\n const hasE2EDir = await fileExists(e2eDir);\n const hasSrcDir = await fileExists(srcDir);\n const hasTestsDir = await fileExists(testsDir);\n\n if (!userConfig && !hasE2EDir && !hasSrcDir && !hasTestsDir) {\n console.error(c('red', 'No test sources found.'));\n console.error('');\n console.error('Expected one of:');\n console.error(' • a jest.config.{ts,js,mjs,cjs,json} in the current directory');\n console.error(' • a ./src/ directory with colocated *.spec.ts(x) files');\n console.error(' • a ./__tests__/ directory with *.spec.ts(x) files');\n console.error(' • a ./e2e/ directory with *.e2e.spec.ts(x) files');\n console.error('');\n console.error('Create one of those, then run:');\n console.error(' frontmcp test');\n process.exit(1);\n }\n\n // Build Jest arguments. With a user config, we delegate; otherwise we\n // write our generated config to a temp file and point Jest at it.\n let configPath: string | undefined;\n if (!userConfig) {\n const config = generateJestConfig(cwd, mergedOpts, testDefaults);\n const tempDir = os.tmpdir();\n configPath = path.join(tempDir, `frontmcp-jest-config-${Date.now()}.json`);\n await writeFile(configPath, JSON.stringify(config, null, 2));\n }\n\n // Explicit narrowing avoids the non-null assertion on userConfig. By\n // construction, the branch above guarantees that exactly one of\n // `configPath` or `userConfig` is set when we get here — but TS can't\n // see through that flow, so an explicit guard keeps the type checker\n // honest without `!`.\n const selectedConfig = configPath ?? userConfig;\n if (!selectedConfig) {\n // Defensive: should be unreachable thanks to the no-sources check above.\n throw new Error('Internal error: no Jest config selected.');\n }\n // Positional test patterns: everything after the `test` command itself.\n const testPatterns = opts._.slice(1);\n const jestArgs = buildJestArgs(selectedConfig, mergedOpts, testPatterns);\n\n console.log(`${c('cyan', '[test]')} running tests in ${path.relative(process.cwd(), cwd) || '.'}`);\n if (resolved.configPath || resolved.configDir) {\n console.log(`${c('gray', '[test]')} config: ${resolved.configPath ?? resolved.configDir}`);\n }\n if (userConfig) {\n console.log(`${c('gray', '[test]')} using user Jest config: ${path.relative(cwd, userConfig)}`);\n } else {\n console.log(`${c('gray', '[test]')} using auto-injected Jest configuration`);\n }\n\n if (mergedOpts.runInBand) {\n console.log(`${c('gray', '[test]')} running tests sequentially (--runInBand)`);\n }\n\n if (mergedOpts.watch) {\n console.log(`${c('gray', '[test]')} watch mode enabled`);\n }\n\n if (mergedOpts.coverage) {\n console.log(`${c('gray', '[test]')} coverage collection enabled`);\n }\n\n console.log(`${c('gray', 'hint:')} press Ctrl+C to stop\\n`);\n\n // Run Jest directly via node_modules/.bin or npx without shell\n // Using shell: false with explicit args array avoids escaping issues\n const jest = spawn('npx', jestArgs, {\n stdio: 'inherit',\n shell: false,\n cwd,\n });\n\n // Handle cleanup\n const cleanup = async (proc?: ChildProcess) => {\n try {\n if (proc) {\n proc.kill('SIGINT');\n }\n } catch {\n // ignore\n }\n // Clean up temp config file (only when we generated one — never the user's).\n if (configPath) {\n try {\n await unlink(configPath);\n } catch {\n // ignore\n }\n }\n };\n\n process.on('SIGINT', () => {\n cleanup(jest).finally(() => {\n process.exit(0);\n });\n });\n\n // Wait for Jest to complete\n try {\n await new Promise<void>((resolve, reject) => {\n jest.on('close', (code) => {\n if (code === 0) {\n resolve();\n } else {\n reject(new Error(`Jest exited with code ${code}`));\n }\n });\n jest.on('error', (err) => {\n reject(err);\n });\n });\n } finally {\n // Clean up temp config file (only when we generated one — never the user's).\n if (configPath) {\n try {\n await unlink(configPath);\n } catch {\n // ignore\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-client snippet emitters (issue #400).
|
|
3
|
+
*
|
|
4
|
+
* Each function takes the resolved config and returns the JSON the user
|
|
5
|
+
* pastes into their client's config file. Format choices match the
|
|
6
|
+
* existing copy-paste snippets in
|
|
7
|
+
* `libs/skills/catalog/frontmcp-deployment/examples/mcp-client-integration/`:
|
|
8
|
+
*
|
|
9
|
+
* claude-code → `~/.config/claude/mcp.json` (or `claude_desktop_config.json`)
|
|
10
|
+
* structure: `{ "mcpServers": { "<name>": { ... } } }`
|
|
11
|
+
* claude-desktop → same structure as claude-code; commonly stored at
|
|
12
|
+
* `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
13
|
+
* cursor / vscode → same structure (`{ "mcpServers": { ... } }`)
|
|
14
|
+
* windsurf → `~/.codeium/windsurf/mcp_config.json`, same shape
|
|
15
|
+
*
|
|
16
|
+
* All four shapes are byte-compatible — the differences are file location +
|
|
17
|
+
* surrounding wrapper, both of which the user handles after pasting.
|
|
18
|
+
*/
|
|
19
|
+
import type { FrontMcpConfigParsed, McpClientName } from '../../config';
|
|
20
|
+
/**
|
|
21
|
+
* Build the user-pasteable snippet for the given client. All five clients
|
|
22
|
+
* use the `{ mcpServers: { <name>: { ... } } }` shape — they differ only in
|
|
23
|
+
* the file the user pastes it into.
|
|
24
|
+
*/
|
|
25
|
+
export declare function emitClientSnippet(client: McpClientName, config: FrontMcpConfigParsed): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP-client snippet emitters (issue #400).
|
|
4
|
+
*
|
|
5
|
+
* Each function takes the resolved config and returns the JSON the user
|
|
6
|
+
* pastes into their client's config file. Format choices match the
|
|
7
|
+
* existing copy-paste snippets in
|
|
8
|
+
* `libs/skills/catalog/frontmcp-deployment/examples/mcp-client-integration/`:
|
|
9
|
+
*
|
|
10
|
+
* claude-code → `~/.config/claude/mcp.json` (or `claude_desktop_config.json`)
|
|
11
|
+
* structure: `{ "mcpServers": { "<name>": { ... } } }`
|
|
12
|
+
* claude-desktop → same structure as claude-code; commonly stored at
|
|
13
|
+
* `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
14
|
+
* cursor / vscode → same structure (`{ "mcpServers": { ... } }`)
|
|
15
|
+
* windsurf → `~/.codeium/windsurf/mcp_config.json`, same shape
|
|
16
|
+
*
|
|
17
|
+
* All four shapes are byte-compatible — the differences are file location +
|
|
18
|
+
* surrounding wrapper, both of which the user handles after pasting.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.emitClientSnippet = emitClientSnippet;
|
|
22
|
+
function buildServerEntry(client, config) {
|
|
23
|
+
const connection = config.clients?.[client];
|
|
24
|
+
if (!connection) {
|
|
25
|
+
throw new Error(`frontmcp.config has no \`clients.${client}\` entry. ` +
|
|
26
|
+
`Add it: \`clients: { '${client}': { transport: '...' , ... } }\``);
|
|
27
|
+
}
|
|
28
|
+
// Stdio: spawn `command` with `args` + `env`. Most MCP clients omit the
|
|
29
|
+
// `transport` field when stdio (it's the default), so we follow suit.
|
|
30
|
+
if (connection.transport === 'stdio') {
|
|
31
|
+
const command = connection.command ?? 'npx';
|
|
32
|
+
const args = connection.args ?? ['-y', config.name];
|
|
33
|
+
const entry = { command, args };
|
|
34
|
+
if (connection.env && Object.keys(connection.env).length > 0)
|
|
35
|
+
entry.env = { ...connection.env };
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
// HTTP / SSE: emit `url` + `transport`. URL falls back to the configured
|
|
39
|
+
// HTTP port when none is provided. We collect every deployment port and
|
|
40
|
+
// only derive a fallback when exactly one is available — picking the
|
|
41
|
+
// first of several would point the user's client at an arbitrary server.
|
|
42
|
+
const deploymentPorts = config.deployments
|
|
43
|
+
.map((d) => ('server' in d ? d.server?.http?.port : undefined))
|
|
44
|
+
.filter((p) => typeof p === 'number');
|
|
45
|
+
const derivedDeploymentPort = deploymentPorts.length === 1 ? deploymentPorts[0] : undefined;
|
|
46
|
+
const httpPort = config.transport?.http?.port ?? derivedDeploymentPort;
|
|
47
|
+
const httpHost = config.transport?.http?.host ?? '127.0.0.1';
|
|
48
|
+
const httpPath = config.transport?.http?.path ?? '/mcp';
|
|
49
|
+
const fallbackUrl = httpPort ? `http://${httpHost}:${httpPort}${httpPath}` : undefined;
|
|
50
|
+
const url = connection.url ?? fallbackUrl;
|
|
51
|
+
if (!url) {
|
|
52
|
+
if (!connection.url && deploymentPorts.length > 1) {
|
|
53
|
+
throw new Error(`frontmcp.config \`clients.${client}.url\` is required when multiple deployment HTTP ports are configured.`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`frontmcp.config \`clients.${client}\` needs a \`url\`, or a \`transport.http.port\` / deployment HTTP port to derive one.`);
|
|
56
|
+
}
|
|
57
|
+
const entry = { url, transport: connection.transport };
|
|
58
|
+
if (connection.env && Object.keys(connection.env).length > 0)
|
|
59
|
+
entry.env = { ...connection.env };
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the user-pasteable snippet for the given client. All five clients
|
|
64
|
+
* use the `{ mcpServers: { <name>: { ... } } }` shape — they differ only in
|
|
65
|
+
* the file the user pastes it into.
|
|
66
|
+
*/
|
|
67
|
+
function emitClientSnippet(client, config) {
|
|
68
|
+
const connection = config.clients?.[client];
|
|
69
|
+
const serverKey = connection?.name ?? config.name;
|
|
70
|
+
const entry = buildServerEntry(client, config);
|
|
71
|
+
const payload = { mcpServers: { [serverKey]: entry } };
|
|
72
|
+
return JSON.stringify(payload, null, 2);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=mcp-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-client.js","sourceRoot":"","sources":["../../../../src/commands/eject/mcp-client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MCP-client snippet emitters (issue #400).\n *\n * Each function takes the resolved config and returns the JSON the user\n * pastes into their client's config file. Format choices match the\n * existing copy-paste snippets in\n * `libs/skills/catalog/frontmcp-deployment/examples/mcp-client-integration/`:\n *\n * claude-code → `~/.config/claude/mcp.json` (or `claude_desktop_config.json`)\n * structure: `{ \"mcpServers\": { \"<name>\": { ... } } }`\n * claude-desktop → same structure as claude-code; commonly stored at\n * `~/Library/Application Support/Claude/claude_desktop_config.json`\n * cursor / vscode → same structure (`{ \"mcpServers\": { ... } }`)\n * windsurf → `~/.codeium/windsurf/mcp_config.json`, same shape\n *\n * All four shapes are byte-compatible — the differences are file location +\n * surrounding wrapper, both of which the user handles after pasting.\n */\n\nimport type { FrontMcpConfigParsed, McpClientName } from '../../config';\n\ninterface ServerEntry {\n command?: string;\n args?: string[];\n env?: Record<string, string>;\n url?: string;\n transport?: 'http' | 'sse' | 'stdio';\n}\n\nfunction buildServerEntry(client: McpClientName, config: FrontMcpConfigParsed): ServerEntry {\n const connection = config.clients?.[client];\n if (!connection) {\n throw new Error(\n `frontmcp.config has no \\`clients.${client}\\` entry. ` +\n `Add it: \\`clients: { '${client}': { transport: '...' , ... } }\\``,\n );\n }\n\n // Stdio: spawn `command` with `args` + `env`. Most MCP clients omit the\n // `transport` field when stdio (it's the default), so we follow suit.\n if (connection.transport === 'stdio') {\n const command = connection.command ?? 'npx';\n const args = connection.args ?? ['-y', config.name];\n const entry: ServerEntry = { command, args };\n if (connection.env && Object.keys(connection.env).length > 0) entry.env = { ...connection.env };\n return entry;\n }\n\n // HTTP / SSE: emit `url` + `transport`. URL falls back to the configured\n // HTTP port when none is provided. We collect every deployment port and\n // only derive a fallback when exactly one is available — picking the\n // first of several would point the user's client at an arbitrary server.\n const deploymentPorts = config.deployments\n .map((d) => ('server' in d ? d.server?.http?.port : undefined))\n .filter((p): p is number => typeof p === 'number');\n const derivedDeploymentPort = deploymentPorts.length === 1 ? deploymentPorts[0] : undefined;\n const httpPort = config.transport?.http?.port ?? derivedDeploymentPort;\n const httpHost = config.transport?.http?.host ?? '127.0.0.1';\n const httpPath = config.transport?.http?.path ?? '/mcp';\n const fallbackUrl = httpPort ? `http://${httpHost}:${httpPort}${httpPath}` : undefined;\n const url = connection.url ?? fallbackUrl;\n if (!url) {\n if (!connection.url && deploymentPorts.length > 1) {\n throw new Error(\n `frontmcp.config \\`clients.${client}.url\\` is required when multiple deployment HTTP ports are configured.`,\n );\n }\n throw new Error(\n `frontmcp.config \\`clients.${client}\\` needs a \\`url\\`, or a \\`transport.http.port\\` / deployment HTTP port to derive one.`,\n );\n }\n const entry: ServerEntry = { url, transport: connection.transport };\n if (connection.env && Object.keys(connection.env).length > 0) entry.env = { ...connection.env };\n return entry;\n}\n\n/**\n * Build the user-pasteable snippet for the given client. All five clients\n * use the `{ mcpServers: { <name>: { ... } } }` shape — they differ only in\n * the file the user pastes it into.\n */\nexport function emitClientSnippet(client: McpClientName, config: FrontMcpConfigParsed): string {\n const connection = config.clients?.[client];\n const serverKey = connection?.name ?? config.name;\n const entry = buildServerEntry(client, config);\n const payload = { mcpServers: { [serverKey]: entry } };\n return JSON.stringify(payload, null, 2);\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `frontmcp eject-mcp-config <client>` (issue #400).
|
|
3
|
+
*
|
|
4
|
+
* Reads `clients.<client>` from the resolved `frontmcp.config` and prints a
|
|
5
|
+
* ready-to-paste MCP client snippet to stdout. Supported clients:
|
|
6
|
+
* `claude-code`, `claude-desktop`, `cursor`, `windsurf`, `vscode`.
|
|
7
|
+
*/
|
|
8
|
+
import type { Command } from 'commander';
|
|
9
|
+
export declare function registerEjectCommands(program: Command): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `frontmcp eject-mcp-config <client>` (issue #400).
|
|
4
|
+
*
|
|
5
|
+
* Reads `clients.<client>` from the resolved `frontmcp.config` and prints a
|
|
6
|
+
* ready-to-paste MCP client snippet to stdout. Supported clients:
|
|
7
|
+
* `claude-code`, `claude-desktop`, `cursor`, `windsurf`, `vscode`.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.registerEjectCommands = registerEjectCommands;
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const path = tslib_1.__importStar(require("path"));
|
|
13
|
+
const utils_1 = require("@frontmcp/utils");
|
|
14
|
+
const config_1 = require("../../config");
|
|
15
|
+
const colors_1 = require("../../core/colors");
|
|
16
|
+
const mcp_client_1 = require("./mcp-client");
|
|
17
|
+
const SUPPORTED_CLIENTS = ['claude-code', 'claude-desktop', 'cursor', 'windsurf', 'vscode'];
|
|
18
|
+
function registerEjectCommands(program) {
|
|
19
|
+
program
|
|
20
|
+
.command('eject-mcp-config <client>')
|
|
21
|
+
.description(`Emit a ready-to-paste MCP client snippet. Supported: ${SUPPORTED_CLIENTS.join(', ')}`)
|
|
22
|
+
.option('-o, --out <path>', 'Write the snippet to a file instead of stdout')
|
|
23
|
+
.option('--dry-run', 'When --out is set, print what would be written instead of writing')
|
|
24
|
+
.action(async (client, opts) => {
|
|
25
|
+
if (!SUPPORTED_CLIENTS.includes(client)) {
|
|
26
|
+
console.error((0, colors_1.c)('red', `Unknown client "${client}".`));
|
|
27
|
+
console.error(`Supported: ${SUPPORTED_CLIENTS.join(', ')}`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
const topLevelOpts = program.opts();
|
|
31
|
+
const resolved = await (0, config_1.resolveConfig)({
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
mode: 'build:ship',
|
|
34
|
+
configPath: topLevelOpts.config,
|
|
35
|
+
});
|
|
36
|
+
if (!resolved.config) {
|
|
37
|
+
console.error((0, colors_1.c)('red', 'No frontmcp.config found.'));
|
|
38
|
+
console.error('Create one with `frontmcp create` or pass --config <path>.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const snippet = (0, mcp_client_1.emitClientSnippet)(client, resolved.config);
|
|
42
|
+
if (opts.out) {
|
|
43
|
+
const target = path.isAbsolute(opts.out) ? opts.out : path.resolve(process.cwd(), opts.out);
|
|
44
|
+
if (opts.dryRun) {
|
|
45
|
+
console.log((0, colors_1.c)('cyan', `[dry-run] would write ${target}:\n`));
|
|
46
|
+
console.log(snippet);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await (0, utils_1.writeFile)(target, snippet);
|
|
50
|
+
console.log((0, colors_1.c)('green', `✓ Wrote ${target}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log(snippet);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../../src/commands/eject/register.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAcH,sDA0CC;;AAtDD,mDAA6B;AAI7B,2CAA4C;AAE5C,yCAAiE;AACjE,8CAAsC;AACtC,6CAAiD;AAEjD,MAAM,iBAAiB,GAAoB,CAAC,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;AAE7G,SAAgB,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,2BAA2B,CAAC;SACpC,WAAW,CAAC,wDAAwD,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;SACnG,MAAM,CAAC,kBAAkB,EAAE,+CAA+C,CAAC;SAC3E,MAAM,CAAC,WAAW,EAAE,mEAAmE,CAAC;SACxF,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,IAAwC,EAAE,EAAE;QACzE,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,MAAuB,CAAC,EAAE,CAAC;YACzD,OAAO,CAAC,KAAK,CAAC,IAAA,UAAC,EAAC,KAAK,EAAE,mBAAmB,MAAM,IAAI,CAAC,CAAC,CAAC;YACvD,OAAO,CAAC,KAAK,CAAC,cAAc,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,EAAyB,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,IAAA,sBAAa,EAAC;YACnC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;YAClB,IAAI,EAAE,YAAY;YAClB,UAAU,EAAE,YAAY,CAAC,MAAM;SAChC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,IAAA,UAAC,EAAC,KAAK,EAAE,2BAA2B,CAAC,CAAC,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;YAC5E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,OAAO,GAAG,IAAA,8BAAiB,EAAC,MAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QAE5E,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YAC5F,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,OAAO,CAAC,GAAG,CAAC,IAAA,UAAC,EAAC,MAAM,EAAE,yBAAyB,MAAM,KAAK,CAAC,CAAC,CAAC;gBAC7D,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO;YACT,CAAC;YACD,MAAM,IAAA,iBAAS,EAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,IAAA,UAAC,EAAC,OAAO,EAAE,WAAW,MAAM,EAAE,CAAC,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACP,CAAC","sourcesContent":["/**\n * `frontmcp eject-mcp-config <client>` (issue #400).\n *\n * Reads `clients.<client>` from the resolved `frontmcp.config` and prints a\n * ready-to-paste MCP client snippet to stdout. Supported clients:\n * `claude-code`, `claude-desktop`, `cursor`, `windsurf`, `vscode`.\n */\n\nimport * as path from 'path';\n\nimport type { Command } from 'commander';\n\nimport { writeFile } from '@frontmcp/utils';\n\nimport { resolveConfig, type McpClientName } from '../../config';\nimport { c } from '../../core/colors';\nimport { emitClientSnippet } from './mcp-client';\n\nconst SUPPORTED_CLIENTS: McpClientName[] = ['claude-code', 'claude-desktop', 'cursor', 'windsurf', 'vscode'];\n\nexport function registerEjectCommands(program: Command): void {\n program\n .command('eject-mcp-config <client>')\n .description(`Emit a ready-to-paste MCP client snippet. Supported: ${SUPPORTED_CLIENTS.join(', ')}`)\n .option('-o, --out <path>', 'Write the snippet to a file instead of stdout')\n .option('--dry-run', 'When --out is set, print what would be written instead of writing')\n .action(async (client: string, opts: { out?: string; dryRun?: boolean }) => {\n if (!SUPPORTED_CLIENTS.includes(client as McpClientName)) {\n console.error(c('red', `Unknown client \"${client}\".`));\n console.error(`Supported: ${SUPPORTED_CLIENTS.join(', ')}`);\n process.exit(2);\n }\n\n const topLevelOpts = program.opts() as { config?: string };\n const resolved = await resolveConfig({\n cwd: process.cwd(),\n mode: 'build:ship',\n configPath: topLevelOpts.config,\n });\n\n if (!resolved.config) {\n console.error(c('red', 'No frontmcp.config found.'));\n console.error('Create one with `frontmcp create` or pass --config <path>.');\n process.exit(1);\n }\n\n const snippet = emitClientSnippet(client as McpClientName, resolved.config);\n\n if (opts.out) {\n const target = path.isAbsolute(opts.out) ? opts.out : path.resolve(process.cwd(), opts.out);\n if (opts.dryRun) {\n console.log(c('cyan', `[dry-run] would write ${target}:\\n`));\n console.log(snippet);\n return;\n }\n await writeFile(target, snippet);\n console.log(c('green', `✓ Wrote ${target}`));\n return;\n }\n\n console.log(snippet);\n });\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementation of `frontmcp install --claude-plugin [--codex]` (issue #411).
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Load frontmcp.config + package.json to determine name/version/description.
|
|
6
|
+
* 2. Spin up a transient direct-mode SDK instance to enumerate skills and
|
|
7
|
+
* prompts (the same surfaces a built bin would expose).
|
|
8
|
+
* 3. Delegate to the shared `plugin-emitter.ts` helper.
|
|
9
|
+
*
|
|
10
|
+
* Status / dry-run paths are short-circuited before the SDK is loaded.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runInstallCurrentProject(rawOpts: Record<string, unknown>): Promise<void>;
|
|
13
|
+
export declare function runUninstallCurrentProject(rawOpts: Record<string, unknown>): Promise<void>;
|