@letsrunit/mcp-server 0.14.4 → 0.15.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 +3 -0
- package/dist/{chunk-XJCBPTOU.js → chunk-CEICWMN3.js} +21 -11
- package/dist/chunk-CEICWMN3.js.map +1 -0
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/{tools-CNEENZR2.js → tools-S474WB47.js} +114 -45
- package/dist/tools-S474WB47.js.map +1 -0
- package/package.json +8 -8
- package/src/bootstrap.ts +20 -9
- package/src/index.ts +5 -3
- package/src/tools/diagnostics.ts +2 -2
- package/src/tools/index.ts +1 -0
- package/src/tools/reload.ts +56 -0
- package/src/tools/session-start.ts +13 -3
- package/src/utility/diagnostics.ts +159 -0
- package/src/utility/support.ts +46 -153
- package/dist/chunk-XJCBPTOU.js.map +0 -1
- package/dist/tools-CNEENZR2.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { bootstrapProjectServer } from './bootstrap';
|
|
4
4
|
|
|
5
|
-
declare const __LETSRUNIT_VERSION__: string;
|
|
5
|
+
declare const __LETSRUNIT_VERSION__: string | undefined;
|
|
6
6
|
|
|
7
7
|
const version = typeof __LETSRUNIT_VERSION__ === 'string' ? __LETSRUNIT_VERSION__ : 'unknown';
|
|
8
|
-
bootstrapProjectServer();
|
|
8
|
+
const runtimeMode = bootstrapProjectServer();
|
|
9
9
|
|
|
10
10
|
const { SessionManager } = await import('./sessions');
|
|
11
11
|
const {
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
registerDiff,
|
|
15
15
|
registerListSteps,
|
|
16
16
|
registerListSessions,
|
|
17
|
+
registerReload,
|
|
17
18
|
registerRun,
|
|
18
19
|
registerScreenshot,
|
|
19
20
|
registerSessionClose,
|
|
@@ -29,7 +30,7 @@ const server = new McpServer({
|
|
|
29
30
|
websiteUrl: 'https://letsrunit.ai',
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
registerSessionStart(server, sessions);
|
|
33
|
+
registerSessionStart(server, sessions, { runtimeMode });
|
|
33
34
|
registerRun(server, sessions);
|
|
34
35
|
registerSnapshot(server, sessions);
|
|
35
36
|
registerScreenshot(server, sessions);
|
|
@@ -37,6 +38,7 @@ registerDebug(server, sessions);
|
|
|
37
38
|
registerSessionClose(server, sessions);
|
|
38
39
|
registerListSteps(server, sessions);
|
|
39
40
|
registerListSessions(server, sessions);
|
|
41
|
+
registerReload(server, { runtimeMode });
|
|
40
42
|
registerDiff(server, sessions);
|
|
41
43
|
if (process.env.LETSRUNIT_MCP_DIAGNOSTICS === 'enabled') {
|
|
42
44
|
registerDiagnostics(server, sessions);
|
package/src/tools/diagnostics.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { SessionManager } from '../sessions';
|
|
4
|
-
import {
|
|
4
|
+
import { collectDiagnostics } from '../utility/diagnostics';
|
|
5
5
|
import { err, text } from '../utility/response';
|
|
6
6
|
|
|
7
7
|
export function registerDiagnostics(server: McpServer, sessions: SessionManager): void {
|
|
@@ -16,7 +16,7 @@ export function registerDiagnostics(server: McpServer, sessions: SessionManager)
|
|
|
16
16
|
},
|
|
17
17
|
async (input) => {
|
|
18
18
|
try {
|
|
19
|
-
const diagnostics = await
|
|
19
|
+
const diagnostics = await collectDiagnostics();
|
|
20
20
|
const session = sessions.get(input.sessionId);
|
|
21
21
|
const sessionInfo = {
|
|
22
22
|
sessionId: session.id,
|
package/src/tools/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { registerDiff } from './diff';
|
|
|
4
4
|
export { registerListSteps } from './list-steps';
|
|
5
5
|
export { registerListSessions } from './list-sessions';
|
|
6
6
|
export { registerRun } from './run';
|
|
7
|
+
export { registerReload } from './reload';
|
|
7
8
|
export { registerScreenshot } from './screenshot';
|
|
8
9
|
export { registerSessionClose } from './session-close';
|
|
9
10
|
export { registerSessionStart } from './session-start';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { McpRuntimeMode } from '../bootstrap';
|
|
4
|
+
import { err, text } from '../utility/response';
|
|
5
|
+
import { reloadSupportFiles } from '../utility/support';
|
|
6
|
+
|
|
7
|
+
type Options = {
|
|
8
|
+
runtimeMode: McpRuntimeMode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function resetBuiltInStepRegistry(): Promise<void> {
|
|
12
|
+
const bdd = (await import('@letsrunit/bdd')) as Record<string, unknown>;
|
|
13
|
+
const reset = bdd.resetRegistryToBuiltInSteps;
|
|
14
|
+
if (typeof reset !== 'function') {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Installed @letsrunit/bdd does not expose resetRegistryToBuiltInSteps. Update @letsrunit/bdd to a compatible version.',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
reset();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerReload(server: McpServer, options: Options): void {
|
|
23
|
+
server.registerTool(
|
|
24
|
+
'letsrunit_reload',
|
|
25
|
+
{
|
|
26
|
+
description:
|
|
27
|
+
'Reload built-in and project support step definitions without restarting the MCP server.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
cwd: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Project directory to resolve cucumber support files from. Defaults to current project cwd.'),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async (input) => {
|
|
36
|
+
if (options.runtimeMode !== 'project') {
|
|
37
|
+
return err('Reload failed: letsrunit_reload is only available in project runtime mode.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await resetBuiltInStepRegistry();
|
|
42
|
+
const result = await reloadSupportFiles(input.cwd);
|
|
43
|
+
return text(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
reloaded: true,
|
|
46
|
+
projectRoot: result.projectRoot,
|
|
47
|
+
supportEntriesLoaded: result.supportEntriesLoaded,
|
|
48
|
+
ignoredEntries: result.ignoredEntries,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return err(`Reload failed: ${(e as Error).message}`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import type { McpRuntimeMode } from '../bootstrap';
|
|
3
4
|
import type { SessionManager } from '../sessions';
|
|
4
5
|
import { err, text } from '../utility/response';
|
|
5
6
|
import { loadSupportFiles } from '../utility/support';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
interface Options {
|
|
9
|
+
runtimeMode: McpRuntimeMode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerSessionStart(server: McpServer, sessions: SessionManager, opts?: Options): void {
|
|
8
13
|
server.registerTool(
|
|
9
14
|
'letsrunit_session_start',
|
|
10
15
|
{
|
|
11
16
|
description:
|
|
12
17
|
'Launch a new browser session. Does not navigate anywhere — use letsrunit_run with a Given step to navigate. Set baseURL to enable relative paths like "Given I\'m on the homepage".',
|
|
13
18
|
inputSchema: {
|
|
14
|
-
baseURL: z
|
|
19
|
+
baseURL: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(
|
|
23
|
+
'Base URL for the session, e.g. "http://localhost:3000". Enables relative paths in Given steps like "Given I\'m on the homepage" or "Given I\'m on page \\"/login\\""',
|
|
24
|
+
),
|
|
15
25
|
language: z.string().optional().describe("Browser language code, e.g. 'en', 'fr'"),
|
|
16
26
|
headless: z.boolean().optional().describe('Run browser in headless mode (default: true)'),
|
|
17
27
|
viewportWidth: z.number().int().optional().describe('Viewport width in pixels (default: 1280)'),
|
|
@@ -20,7 +30,7 @@ export function registerSessionStart(server: McpServer, sessions: SessionManager
|
|
|
20
30
|
},
|
|
21
31
|
async (input) => {
|
|
22
32
|
try {
|
|
23
|
-
if (
|
|
33
|
+
if (opts?.runtimeMode === 'project') {
|
|
24
34
|
await loadSupportFiles();
|
|
25
35
|
}
|
|
26
36
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { loadConfiguration } from '@cucumber/cucumber/api';
|
|
2
|
+
import { registry } from '@letsrunit/bdd';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { decideHandoff, resolveRuntimeModeOverride } from '../bootstrap';
|
|
8
|
+
import {
|
|
9
|
+
expandPathPatterns,
|
|
10
|
+
findCucumberConfig,
|
|
11
|
+
getSupportLoadState,
|
|
12
|
+
loadLetsrunitIgnorePatterns,
|
|
13
|
+
resolveEffectiveCwd,
|
|
14
|
+
resolveSupportEntries,
|
|
15
|
+
type SupportEntry,
|
|
16
|
+
} from './support';
|
|
17
|
+
|
|
18
|
+
declare const __LETSRUNIT_VERSION__: string | undefined;
|
|
19
|
+
|
|
20
|
+
export type Diagnostics = {
|
|
21
|
+
envProjectCwd: string | null;
|
|
22
|
+
processCwd: string;
|
|
23
|
+
inputCwd: string | null;
|
|
24
|
+
effectiveCwd: string;
|
|
25
|
+
projectRoot: string;
|
|
26
|
+
cucumberConfigPath: string | null;
|
|
27
|
+
supportPatterns: string[];
|
|
28
|
+
ignorePatterns: string[];
|
|
29
|
+
ignoredPaths: string[];
|
|
30
|
+
supportEntries: SupportEntry[];
|
|
31
|
+
loadedProjectRoots: string[];
|
|
32
|
+
loadedSupportEntries: string[];
|
|
33
|
+
mcpServer: {
|
|
34
|
+
version: string;
|
|
35
|
+
executablePath: string | null;
|
|
36
|
+
projectServerUsed: boolean;
|
|
37
|
+
handoffDecision: {
|
|
38
|
+
shouldHandoff: boolean;
|
|
39
|
+
runtimeMode: string;
|
|
40
|
+
};
|
|
41
|
+
serverMcpPath: string | null;
|
|
42
|
+
projectMcpPath: string | null;
|
|
43
|
+
};
|
|
44
|
+
letsrunitEnv: Record<string, string>;
|
|
45
|
+
moduleResolution: {
|
|
46
|
+
serverBddPath: string | null;
|
|
47
|
+
projectBddPath: string | null;
|
|
48
|
+
};
|
|
49
|
+
registry: {
|
|
50
|
+
total: number;
|
|
51
|
+
byType: {
|
|
52
|
+
Given: number;
|
|
53
|
+
When: number;
|
|
54
|
+
Then: number;
|
|
55
|
+
};
|
|
56
|
+
definitions: Array<{
|
|
57
|
+
type: 'Given' | 'When' | 'Then';
|
|
58
|
+
source: string;
|
|
59
|
+
comment?: string;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function resolveFrom(moduleId: string, fromPath: string): string | null {
|
|
65
|
+
try {
|
|
66
|
+
const req = createRequire(fromPath);
|
|
67
|
+
return req.resolve(moduleId);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toRealpath(path: string | null): string | null {
|
|
74
|
+
if (!path) return null;
|
|
75
|
+
try {
|
|
76
|
+
return realpathSync(path);
|
|
77
|
+
} catch {
|
|
78
|
+
return path;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pickLetsrunitEnv(): Record<string, string> {
|
|
83
|
+
return Object.fromEntries(
|
|
84
|
+
Object.entries(process.env)
|
|
85
|
+
.filter(([key, value]) => key.startsWith('LETSRUNIT_') && typeof value === 'string')
|
|
86
|
+
.map(([key, value]) => [key, value as string]),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function collectDiagnostics(cwd?: string): Promise<Diagnostics> {
|
|
91
|
+
const effectiveCwd = resolveEffectiveCwd(cwd);
|
|
92
|
+
const projectRoot = resolve(effectiveCwd);
|
|
93
|
+
const cucumberConfigPath = findCucumberConfig(projectRoot);
|
|
94
|
+
const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
|
|
95
|
+
const supportPatterns = [...(useConfiguration.require ?? []), ...(useConfiguration.import ?? [])];
|
|
96
|
+
const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
|
|
97
|
+
const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
|
|
98
|
+
const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
|
|
99
|
+
const supportLoadState = getSupportLoadState();
|
|
100
|
+
const serverBddPath = toRealpath(resolveFrom('@letsrunit/bdd', import.meta.url));
|
|
101
|
+
const projectBddPath = toRealpath(resolveFrom('@letsrunit/bdd', resolve(projectRoot, 'package.json')));
|
|
102
|
+
const projectMcpEntryPath = resolveFrom('@letsrunit/mcp-server', resolve(projectRoot, 'package.json'));
|
|
103
|
+
const currentEntrypointPath = toRealpath(fileURLToPath(import.meta.url));
|
|
104
|
+
const projectEntrypointPath = toRealpath(projectMcpEntryPath);
|
|
105
|
+
const handoffDecision = decideHandoff(
|
|
106
|
+
currentEntrypointPath,
|
|
107
|
+
projectEntrypointPath,
|
|
108
|
+
resolveRuntimeModeOverride(),
|
|
109
|
+
);
|
|
110
|
+
const serverMcpPath = toRealpath(resolveFrom('@letsrunit/mcp-server', import.meta.url));
|
|
111
|
+
const projectMcpPath = toRealpath(projectMcpEntryPath);
|
|
112
|
+
const executablePath = toRealpath(process.argv[1] ?? null);
|
|
113
|
+
const version = typeof __LETSRUNIT_VERSION__ === 'string' ? __LETSRUNIT_VERSION__ : 'unknown';
|
|
114
|
+
const registryDefinitions = registry.defs.map((def) => ({
|
|
115
|
+
type: def.type,
|
|
116
|
+
source: def.source,
|
|
117
|
+
comment: def.comment,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
envProjectCwd: process.env.LETSRUNIT_PROJECT_CWD ?? null,
|
|
122
|
+
processCwd: process.cwd(),
|
|
123
|
+
inputCwd: cwd ?? null,
|
|
124
|
+
effectiveCwd,
|
|
125
|
+
projectRoot,
|
|
126
|
+
cucumberConfigPath,
|
|
127
|
+
supportPatterns,
|
|
128
|
+
ignorePatterns,
|
|
129
|
+
ignoredPaths: [...ignoredPaths].sort(),
|
|
130
|
+
supportEntries,
|
|
131
|
+
loadedProjectRoots: supportLoadState.loadedProjectRoots,
|
|
132
|
+
loadedSupportEntries: supportLoadState.loadedSupportEntries,
|
|
133
|
+
mcpServer: {
|
|
134
|
+
version,
|
|
135
|
+
executablePath,
|
|
136
|
+
projectServerUsed: handoffDecision.runtimeMode === 'project',
|
|
137
|
+
handoffDecision: {
|
|
138
|
+
shouldHandoff: handoffDecision.shouldHandoff,
|
|
139
|
+
runtimeMode: handoffDecision.runtimeMode,
|
|
140
|
+
},
|
|
141
|
+
serverMcpPath,
|
|
142
|
+
projectMcpPath,
|
|
143
|
+
},
|
|
144
|
+
letsrunitEnv: pickLetsrunitEnv(),
|
|
145
|
+
moduleResolution: {
|
|
146
|
+
serverBddPath,
|
|
147
|
+
projectBddPath,
|
|
148
|
+
},
|
|
149
|
+
registry: {
|
|
150
|
+
total: registryDefinitions.length,
|
|
151
|
+
byType: {
|
|
152
|
+
Given: registryDefinitions.filter((d) => d.type === 'Given').length,
|
|
153
|
+
When: registryDefinitions.filter((d) => d.type === 'When').length,
|
|
154
|
+
Then: registryDefinitions.filter((d) => d.type === 'Then').length,
|
|
155
|
+
},
|
|
156
|
+
definitions: registryDefinitions,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
package/src/utility/support.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { loadConfiguration } from '@cucumber/cucumber/api';
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
4
3
|
import { glob } from 'node:fs/promises';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
4
|
import { isAbsolute, resolve } from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
import { decideHandoff } from '../bootstrap';
|
|
9
|
-
|
|
10
|
-
declare const __LETSRUNIT_VERSION__: string;
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
11
6
|
|
|
12
7
|
type CucumberConfig = {
|
|
13
8
|
require?: unknown;
|
|
@@ -17,7 +12,7 @@ type CucumberConfig = {
|
|
|
17
12
|
};
|
|
18
13
|
};
|
|
19
14
|
|
|
20
|
-
type SupportEntry = { kind: 'path'; value: string } | { kind: 'module'; value: string };
|
|
15
|
+
export type SupportEntry = { kind: 'path'; value: string } | { kind: 'module'; value: string };
|
|
21
16
|
|
|
22
17
|
const CUCUMBER_CONFIG_FILES = [
|
|
23
18
|
'cucumber.js',
|
|
@@ -31,48 +26,14 @@ const CUCUMBER_CONFIG_FILES = [
|
|
|
31
26
|
const loadedProjectRoots = new Set<string>();
|
|
32
27
|
const loadedSupportEntries = new Set<string>();
|
|
33
28
|
|
|
34
|
-
export type
|
|
35
|
-
envProjectCwd: string | null;
|
|
36
|
-
processCwd: string;
|
|
37
|
-
inputCwd: string | null;
|
|
38
|
-
effectiveCwd: string;
|
|
29
|
+
export type SupportLoadResult = {
|
|
39
30
|
projectRoot: string;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
loadedSupportEntries: string[];
|
|
47
|
-
mcpServer: {
|
|
48
|
-
version: string;
|
|
49
|
-
executablePath: string | null;
|
|
50
|
-
projectServerUsed: boolean;
|
|
51
|
-
handoffDecision: {
|
|
52
|
-
shouldHandoff: boolean;
|
|
53
|
-
runtimeMode: string;
|
|
54
|
-
};
|
|
55
|
-
serverMcpPath: string | null;
|
|
56
|
-
projectMcpPath: string | null;
|
|
57
|
-
};
|
|
58
|
-
letsrunitEnv: Record<string, string>;
|
|
59
|
-
moduleResolution: {
|
|
60
|
-
serverBddPath: string | null;
|
|
61
|
-
projectBddPath: string | null;
|
|
62
|
-
};
|
|
63
|
-
registry: {
|
|
64
|
-
total: number;
|
|
65
|
-
byType: {
|
|
66
|
-
Given: number;
|
|
67
|
-
When: number;
|
|
68
|
-
Then: number;
|
|
69
|
-
};
|
|
70
|
-
definitions: Array<{
|
|
71
|
-
type: 'Given' | 'When' | 'Then';
|
|
72
|
-
source: string;
|
|
73
|
-
comment?: string;
|
|
74
|
-
}>;
|
|
75
|
-
};
|
|
31
|
+
supportEntriesLoaded: number;
|
|
32
|
+
ignoredEntries: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type LoadSupportOptions = {
|
|
36
|
+
forceReload?: boolean;
|
|
76
37
|
};
|
|
77
38
|
|
|
78
39
|
function toStrings(value: unknown): string[] {
|
|
@@ -96,7 +57,7 @@ function normalizeMatch(baseDir: string, match: string): string {
|
|
|
96
57
|
return isAbsolute(match) ? resolve(match) : resolve(baseDir, match);
|
|
97
58
|
}
|
|
98
59
|
|
|
99
|
-
async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<Set<string>> {
|
|
60
|
+
export async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<Set<string>> {
|
|
100
61
|
const files = new Set<string>();
|
|
101
62
|
|
|
102
63
|
for (const pattern of patterns) {
|
|
@@ -113,7 +74,7 @@ async function expandPathPatterns(baseDir: string, patterns: string[]): Promise<
|
|
|
113
74
|
return files;
|
|
114
75
|
}
|
|
115
76
|
|
|
116
|
-
async function resolveSupportEntries(baseDir: string, entries: string[]): Promise<SupportEntry[]> {
|
|
77
|
+
export async function resolveSupportEntries(baseDir: string, entries: string[]): Promise<SupportEntry[]> {
|
|
117
78
|
const resolved: SupportEntry[] = [];
|
|
118
79
|
|
|
119
80
|
for (const entry of entries) {
|
|
@@ -135,7 +96,7 @@ async function resolveSupportEntries(baseDir: string, entries: string[]): Promis
|
|
|
135
96
|
return resolved;
|
|
136
97
|
}
|
|
137
98
|
|
|
138
|
-
function findCucumberConfig(cwd: string): string | null {
|
|
99
|
+
export function findCucumberConfig(cwd: string): string | null {
|
|
139
100
|
for (const filename of CUCUMBER_CONFIG_FILES) {
|
|
140
101
|
const path = resolve(cwd, filename);
|
|
141
102
|
if (existsSync(path)) return path;
|
|
@@ -144,7 +105,7 @@ function findCucumberConfig(cwd: string): string | null {
|
|
|
144
105
|
return null;
|
|
145
106
|
}
|
|
146
107
|
|
|
147
|
-
async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
|
|
108
|
+
export async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
|
|
148
109
|
const configPath = findCucumberConfig(cwd);
|
|
149
110
|
if (!configPath) return [];
|
|
150
111
|
|
|
@@ -154,141 +115,73 @@ async function loadLetsrunitIgnorePatterns(cwd: string): Promise<string[]> {
|
|
|
154
115
|
return toStrings(config.letsrunit?.ignore);
|
|
155
116
|
}
|
|
156
117
|
|
|
157
|
-
function resolveEffectiveCwd(cwd?: string): string {
|
|
118
|
+
export function resolveEffectiveCwd(cwd?: string): string {
|
|
158
119
|
return cwd ?? process.env.LETSRUNIT_PROJECT_CWD ?? process.cwd();
|
|
159
120
|
}
|
|
160
121
|
|
|
161
|
-
function
|
|
162
|
-
try {
|
|
163
|
-
const req = createRequire(fromPath);
|
|
164
|
-
return req.resolve(moduleId);
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function toRealpath(path: string | null): string | null {
|
|
171
|
-
if (!path) return null;
|
|
172
|
-
try {
|
|
173
|
-
return realpathSync(path);
|
|
174
|
-
} catch {
|
|
175
|
-
return path;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function pickLetsrunitEnv(): Record<string, string> {
|
|
180
|
-
return Object.fromEntries(
|
|
181
|
-
Object.entries(process.env)
|
|
182
|
-
.filter(([key, value]) => key.startsWith('LETSRUNIT_') && typeof value === 'string')
|
|
183
|
-
.map(([key, value]) => [key, value as string]),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function resolveMcpServerVersion(): string {
|
|
188
|
-
return typeof __LETSRUNIT_VERSION__ === 'string' ? __LETSRUNIT_VERSION__ : 'unknown';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function collectSupportDiagnostics(cwd?: string): Promise<SupportDiagnostics> {
|
|
192
|
-
const effectiveCwd = resolveEffectiveCwd(cwd);
|
|
193
|
-
const projectRoot = resolve(effectiveCwd);
|
|
194
|
-
const cucumberConfigPath = findCucumberConfig(projectRoot);
|
|
195
|
-
const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
|
|
196
|
-
const supportPatterns = [...toStrings(useConfiguration.require), ...toStrings(useConfiguration.import)];
|
|
197
|
-
const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
|
|
198
|
-
const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
|
|
199
|
-
const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
|
|
200
|
-
const serverBddPath = toRealpath(resolveFrom('@letsrunit/bdd', import.meta.url));
|
|
201
|
-
const projectBddPath = toRealpath(resolveFrom('@letsrunit/bdd', resolve(projectRoot, 'package.json')));
|
|
202
|
-
const projectMcpEntryPath = resolveFrom('@letsrunit/mcp-server', resolve(projectRoot, 'package.json'));
|
|
203
|
-
const currentEntrypointPath = toRealpath(fileURLToPath(import.meta.url));
|
|
204
|
-
const projectEntrypointPath = toRealpath(projectMcpEntryPath);
|
|
205
|
-
const handoffDecision = decideHandoff(
|
|
206
|
-
currentEntrypointPath,
|
|
207
|
-
projectEntrypointPath,
|
|
208
|
-
process.env.LETSRUNIT_MCP_BOOTSTRAPPED === '1',
|
|
209
|
-
);
|
|
210
|
-
const serverMcpPath = toRealpath(resolveFrom('@letsrunit/mcp-server', import.meta.url));
|
|
211
|
-
const projectMcpPath = toRealpath(projectMcpEntryPath);
|
|
212
|
-
const executablePath = toRealpath(process.argv[1] ?? null);
|
|
213
|
-
const version = resolveMcpServerVersion();
|
|
214
|
-
const registryDefinitions = registry.defs.map((def) => ({
|
|
215
|
-
type: def.type,
|
|
216
|
-
source: def.source,
|
|
217
|
-
comment: def.comment,
|
|
218
|
-
}));
|
|
219
|
-
|
|
122
|
+
export function getSupportLoadState(): { loadedProjectRoots: string[]; loadedSupportEntries: string[] } {
|
|
220
123
|
return {
|
|
221
|
-
envProjectCwd: process.env.LETSRUNIT_PROJECT_CWD ?? null,
|
|
222
|
-
processCwd: process.cwd(),
|
|
223
|
-
inputCwd: cwd ?? null,
|
|
224
|
-
effectiveCwd,
|
|
225
|
-
projectRoot,
|
|
226
|
-
cucumberConfigPath,
|
|
227
|
-
supportPatterns,
|
|
228
|
-
ignorePatterns,
|
|
229
|
-
ignoredPaths: [...ignoredPaths].sort(),
|
|
230
|
-
supportEntries,
|
|
231
124
|
loadedProjectRoots: [...loadedProjectRoots].sort(),
|
|
232
125
|
loadedSupportEntries: [...loadedSupportEntries].sort(),
|
|
233
|
-
mcpServer: {
|
|
234
|
-
version,
|
|
235
|
-
executablePath,
|
|
236
|
-
projectServerUsed: handoffDecision.runtimeMode === 'project',
|
|
237
|
-
handoffDecision: {
|
|
238
|
-
shouldHandoff: handoffDecision.shouldHandoff,
|
|
239
|
-
runtimeMode: handoffDecision.runtimeMode,
|
|
240
|
-
},
|
|
241
|
-
serverMcpPath,
|
|
242
|
-
projectMcpPath,
|
|
243
|
-
},
|
|
244
|
-
letsrunitEnv: pickLetsrunitEnv(),
|
|
245
|
-
moduleResolution: {
|
|
246
|
-
serverBddPath,
|
|
247
|
-
projectBddPath,
|
|
248
|
-
},
|
|
249
|
-
registry: {
|
|
250
|
-
total: registryDefinitions.length,
|
|
251
|
-
byType: {
|
|
252
|
-
Given: registryDefinitions.filter((d) => d.type === 'Given').length,
|
|
253
|
-
When: registryDefinitions.filter((d) => d.type === 'When').length,
|
|
254
|
-
Then: registryDefinitions.filter((d) => d.type === 'Then').length,
|
|
255
|
-
},
|
|
256
|
-
definitions: registryDefinitions,
|
|
257
|
-
},
|
|
258
126
|
};
|
|
259
127
|
}
|
|
260
128
|
|
|
261
|
-
export
|
|
129
|
+
export function clearSupportLoadState(): void {
|
|
130
|
+
loadedProjectRoots.clear();
|
|
131
|
+
loadedSupportEntries.clear();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildReloadedFileUrl(path: string): string {
|
|
135
|
+
const url = pathToFileURL(path);
|
|
136
|
+
url.searchParams.set('letsrunitReload', Date.now().toString(36));
|
|
137
|
+
return url.href;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function loadSupportFiles(cwd?: string, options?: LoadSupportOptions): Promise<SupportLoadResult> {
|
|
262
141
|
const projectRoot = resolve(resolveEffectiveCwd(cwd));
|
|
263
|
-
|
|
142
|
+
const forceReload = options?.forceReload === true;
|
|
143
|
+
|
|
144
|
+
if (!forceReload && loadedProjectRoots.has(projectRoot)) {
|
|
145
|
+
return { projectRoot, supportEntriesLoaded: 0, ignoredEntries: 0 };
|
|
146
|
+
}
|
|
264
147
|
|
|
265
148
|
const { useConfiguration } = await loadConfiguration({}, { cwd: projectRoot });
|
|
266
149
|
const supportPatterns = [...toStrings(useConfiguration.require), ...toStrings(useConfiguration.import)];
|
|
267
150
|
if (supportPatterns.length === 0) {
|
|
268
151
|
loadedProjectRoots.add(projectRoot);
|
|
269
|
-
return;
|
|
152
|
+
return { projectRoot, supportEntriesLoaded: 0, ignoredEntries: 0 };
|
|
270
153
|
}
|
|
271
154
|
|
|
272
155
|
const ignorePatterns = await loadLetsrunitIgnorePatterns(projectRoot);
|
|
273
156
|
const ignoredPaths = await expandPathPatterns(projectRoot, ignorePatterns);
|
|
274
157
|
const supportEntries = await resolveSupportEntries(projectRoot, supportPatterns);
|
|
158
|
+
let supportEntriesLoaded = 0;
|
|
159
|
+
let ignoredEntries = 0;
|
|
275
160
|
|
|
276
161
|
for (const entry of supportEntries) {
|
|
277
162
|
if (entry.kind === 'path' && ignoredPaths.has(entry.value)) {
|
|
163
|
+
ignoredEntries += 1;
|
|
278
164
|
continue;
|
|
279
165
|
}
|
|
280
166
|
|
|
281
167
|
const key = `${entry.kind}:${entry.value}`;
|
|
282
|
-
if (loadedSupportEntries.has(key)) continue;
|
|
168
|
+
if (!forceReload && loadedSupportEntries.has(key)) continue;
|
|
283
169
|
|
|
284
170
|
if (entry.kind === 'path') {
|
|
285
|
-
await import(pathToFileURL(entry.value).href);
|
|
171
|
+
await import(forceReload ? buildReloadedFileUrl(entry.value) : pathToFileURL(entry.value).href);
|
|
286
172
|
} else {
|
|
287
173
|
await import(entry.value);
|
|
288
174
|
}
|
|
289
175
|
|
|
176
|
+
supportEntriesLoaded += 1;
|
|
290
177
|
loadedSupportEntries.add(key);
|
|
291
178
|
}
|
|
292
179
|
|
|
293
180
|
loadedProjectRoots.add(projectRoot);
|
|
181
|
+
return { projectRoot, supportEntriesLoaded, ignoredEntries };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function reloadSupportFiles(cwd?: string): Promise<SupportLoadResult> {
|
|
185
|
+
clearSupportLoadState();
|
|
186
|
+
return loadSupportFiles(cwd, { forceReload: true });
|
|
294
187
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bootstrap.ts"],"names":[],"mappings":"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","file":"chunk-XJCBPTOU.js","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { realpathSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport type McpRuntimeMode = 'project' | 'standalone';\n\nexport type HandoffDecision = {\n shouldHandoff: boolean;\n runtimeMode: McpRuntimeMode;\n};\n\nfunction resolveProjectRoot(): string {\n return resolve(process.env.LETSRUNIT_PROJECT_CWD ?? process.cwd());\n}\n\nfunction resolveFromProject(moduleId: string, projectRoot: string): string | null {\n try {\n const req = createRequire(resolve(projectRoot, 'package.json'));\n return req.resolve(moduleId);\n } catch {\n return null;\n }\n}\n\nfunction toRealpath(path: string | null): string | null {\n if (!path) return null;\n try {\n return realpathSync(path);\n } catch {\n return null;\n }\n}\n\nfunction sameEntrypoint(a: string | null, b: string | null): boolean {\n if (!a || !b) return false;\n return a === b;\n}\n\nexport function decideHandoff(\n currentEntrypointPath: string | null,\n projectEntrypointPath: string | null,\n isBootstrapped: boolean,\n): HandoffDecision {\n if (!projectEntrypointPath) {\n return { shouldHandoff: false, runtimeMode: 'standalone' };\n }\n\n if (sameEntrypoint(currentEntrypointPath, projectEntrypointPath)) {\n return { shouldHandoff: false, runtimeMode: 'project' };\n }\n\n if (isBootstrapped) {\n return { shouldHandoff: false, runtimeMode: 'standalone' };\n }\n\n return { shouldHandoff: true, runtimeMode: 'project' };\n}\n\nfunction runProjectLocalServer(projectEntrypointPath: string): never {\n const result = spawnSync(process.execPath, [projectEntrypointPath, ...process.argv.slice(2)], {\n stdio: 'inherit',\n env: {\n ...process.env,\n LETSRUNIT_MCP_BOOTSTRAPPED: '1',\n LETSRUNIT_MCP_RUNTIME_MODE: 'project',\n },\n });\n\n if (result.error) throw result.error;\n process.exit(result.status ?? 1);\n}\n\nexport function bootstrapProjectServer(): McpRuntimeMode {\n const projectRoot = resolveProjectRoot();\n const isBootstrapped = process.env.LETSRUNIT_MCP_BOOTSTRAPPED === '1';\n\n const currentEntryPath = toRealpath(fileURLToPath(import.meta.url));\n const projectEntryPath = toRealpath(resolveFromProject('@letsrunit/mcp-server', projectRoot));\n\n const decision = decideHandoff(currentEntryPath, projectEntryPath, isBootstrapped);\n\n if (decision.shouldHandoff && projectEntryPath) {\n runProjectLocalServer(projectEntryPath);\n }\n\n process.env.LETSRUNIT_MCP_RUNTIME_MODE = decision.runtimeMode;\n return decision.runtimeMode;\n}\n"]}
|