@lensmcp/nx-plugin 1.1.0 → 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/executors/agent-build/agent-build.js +1 -1
- package/executors/agent-dev/agent-dev.d.ts +6 -3
- package/executors/agent-dev/agent-dev.d.ts.map +1 -1
- package/executors/agent-dev/agent-dev.js +217 -119
- package/executors/agent-dev/schema.d.ts +7 -1
- package/executors/agent-dev/schema.json +15 -1
- package/lens-frontend.d.ts +72 -0
- package/lens-frontend.d.ts.map +1 -0
- package/lens-frontend.js +193 -0
- package/package.json +6 -1
|
@@ -58,7 +58,7 @@ async function agentBuildExecutor(options, context) {
|
|
|
58
58
|
buildOutDir: (0, node_path_1.join)(projectRoot, 'dist'),
|
|
59
59
|
timestamp: new Date().toISOString(),
|
|
60
60
|
note: 'Phase 2 baseline marker. Per-chunk JSON is materialised by the running ' +
|
|
61
|
-
'lensmcp-mcp session
|
|
61
|
+
'lensmcp-mcp session (the self-contained bundle) once the supervisor wires the plugin bus.',
|
|
62
62
|
}, null, 2) + '\n');
|
|
63
63
|
console.log(`[agent-build] baseline written: ${baselinePath}`);
|
|
64
64
|
}
|
|
@@ -12,9 +12,12 @@ interface ExecutorResult {
|
|
|
12
12
|
* collectors see what the user sees.
|
|
13
13
|
* 3. The LensMCP MCP server, so an agent can connect.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* The MCP server and the browser-capture runner are both spawned from the
|
|
16
|
+
* self-contained `lensmcp` CLI bundle (`bundled/main.js` +
|
|
17
|
+
* `bundled/capture-runner.js`), which inlines every internal `@lensmcp/*`
|
|
18
|
+
* lib — so the executor itself imports NONE of them and a host needs only
|
|
19
|
+
* `npm i @lensmcp/nx-plugin lensmcp` (no unpublished workspace libs, no
|
|
20
|
+
* symlink bridging).
|
|
18
21
|
*
|
|
19
22
|
* Streams everyone's stdio to this terminal so the developer can see
|
|
20
23
|
* what's happening. Ctrl-C triggers a clean teardown.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-dev.d.ts","sourceRoot":"","sources":["../../../src/executors/agent-dev/agent-dev.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-dev.d.ts","sourceRoot":"","sources":["../../../src/executors/agent-dev/agent-dev.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CA+MzB"}
|
|
@@ -4,6 +4,7 @@ exports.default = agentDevExecutor;
|
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
|
+
const lens_frontend_1 = require("../../lens-frontend");
|
|
7
8
|
/**
|
|
8
9
|
* `agent-dev` — long-running executor that spawns:
|
|
9
10
|
*
|
|
@@ -13,9 +14,12 @@ const node_path_1 = require("node:path");
|
|
|
13
14
|
* collectors see what the user sees.
|
|
14
15
|
* 3. The LensMCP MCP server, so an agent can connect.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* The MCP server and the browser-capture runner are both spawned from the
|
|
18
|
+
* self-contained `lensmcp` CLI bundle (`bundled/main.js` +
|
|
19
|
+
* `bundled/capture-runner.js`), which inlines every internal `@lensmcp/*`
|
|
20
|
+
* lib — so the executor itself imports NONE of them and a host needs only
|
|
21
|
+
* `npm i @lensmcp/nx-plugin lensmcp` (no unpublished workspace libs, no
|
|
22
|
+
* symlink bridging).
|
|
19
23
|
*
|
|
20
24
|
* Streams everyone's stdio to this terminal so the developer can see
|
|
21
25
|
* what's happening. Ctrl-C triggers a clean teardown.
|
|
@@ -25,13 +29,16 @@ async function agentDevExecutor(options, context) {
|
|
|
25
29
|
kind: options.kind ?? 'vite-react',
|
|
26
30
|
chrome: options.chrome ?? true,
|
|
27
31
|
headless: options.headless ?? true,
|
|
32
|
+
node: options.node ?? true,
|
|
28
33
|
devTarget: options.devTarget,
|
|
29
34
|
projectRoot: options.projectRoot,
|
|
30
35
|
port: options.port,
|
|
31
36
|
openUrl: options.openUrl,
|
|
37
|
+
devCommand: options.devCommand,
|
|
38
|
+
wsPort: options.wsPort,
|
|
32
39
|
};
|
|
33
|
-
if (opts.kind !== 'vite-react' && opts.kind !== 'nestjs') {
|
|
34
|
-
console.error(`[agent-dev]
|
|
40
|
+
if (opts.kind !== 'vite-react' && opts.kind !== 'nestjs' && opts.kind !== 'imported') {
|
|
41
|
+
console.error(`[agent-dev] supported kinds are "vite-react", "nestjs", "imported" (got "${opts.kind}").`);
|
|
35
42
|
return { success: false };
|
|
36
43
|
}
|
|
37
44
|
const project = context.projectName;
|
|
@@ -44,31 +51,30 @@ async function agentDevExecutor(options, context) {
|
|
|
44
51
|
if (opts.kind === 'nestjs') {
|
|
45
52
|
return runNestjs(projectRoot, context);
|
|
46
53
|
}
|
|
47
|
-
// ----
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'--config',
|
|
59
|
-
resolveFirstExisting(['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs', 'vite.config.cts', 'vite.config.cjs'], projectRoot) ?? (0, node_path_1.join)(projectRoot, 'vite.config.ts'),
|
|
60
|
-
projectRoot,
|
|
61
|
-
];
|
|
62
|
-
if (devPort !== undefined) {
|
|
63
|
-
viteArgs.push('--port', String(devPort), '--strictPort');
|
|
54
|
+
// ---- Imported / bring-your-own-build branch ----
|
|
55
|
+
// (webpack, Next.js, custom builder, or no build at all)
|
|
56
|
+
if (opts.kind === 'imported') {
|
|
57
|
+
return runImported(projectRoot, context, {
|
|
58
|
+
devCommand: opts.devCommand,
|
|
59
|
+
chrome: opts.chrome,
|
|
60
|
+
headless: opts.headless,
|
|
61
|
+
openUrl: opts.openUrl,
|
|
62
|
+
wsPort: opts.wsPort ?? 5747,
|
|
63
|
+
node: opts.node,
|
|
64
|
+
});
|
|
64
65
|
}
|
|
66
|
+
// ---- Vite/React branch (Phase 1) ----
|
|
67
|
+
// The per-app half (Vite + browser capture) is the SHARED helper
|
|
68
|
+
// `spawnLensFrontend`, which the cluster gateway also calls for `lens:true`
|
|
69
|
+
// apps. agent-dev additionally owns the cluster-wide SINGLETONS (the MCP
|
|
70
|
+
// server below) since, standalone, nothing else does.
|
|
65
71
|
// 2. LensMCP MCP — prebuilt bundle from `apps/lensmcp-mcp/dist/main.js`.
|
|
66
72
|
// In a host workspace post-`lensmcp install`, this is the bundled
|
|
67
73
|
// CLI binary that ships with `lensmcp`. For dev in-repo we
|
|
68
74
|
// locate the workspace's own build artefact.
|
|
69
|
-
const mcpBundle = findMcpBundle(context.root);
|
|
75
|
+
const mcpBundle = (0, lens_frontend_1.findMcpBundle)(context.root);
|
|
70
76
|
if (!mcpBundle) {
|
|
71
|
-
console.warn(
|
|
77
|
+
console.warn("[agent-dev] No lensmcp-mcp bundle found — skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/main.js`).");
|
|
72
78
|
}
|
|
73
79
|
const children = [];
|
|
74
80
|
const respawnCounts = new Map();
|
|
@@ -144,8 +150,28 @@ async function agentDevExecutor(options, context) {
|
|
|
144
150
|
}
|
|
145
151
|
const bridgeEnv = { LENSMCP_EVENT_FILE: eventFile };
|
|
146
152
|
console.log(`[agent-dev] event bridge → ${eventFile}`);
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
// The per-app half — Vite + browser capture — via the SHARED helper (the
|
|
154
|
+
// cluster gateway calls the identical function for its `lens:true` apps).
|
|
155
|
+
try {
|
|
156
|
+
(0, lens_frontend_1.spawnLensFrontend)({
|
|
157
|
+
projectRoot,
|
|
158
|
+
workspaceRoot: context.root,
|
|
159
|
+
...(project ? { project } : {}),
|
|
160
|
+
eventFile,
|
|
161
|
+
...(opts.port !== undefined ? { port: opts.port } : {}),
|
|
162
|
+
chrome: opts.chrome,
|
|
163
|
+
headless: opts.headless,
|
|
164
|
+
...(opts.openUrl ? { openUrl: opts.openUrl } : {}),
|
|
165
|
+
log: (l) => console.log(l),
|
|
166
|
+
}, spawnChild);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.error(e.message);
|
|
170
|
+
shutdown();
|
|
171
|
+
return { success: false };
|
|
172
|
+
}
|
|
173
|
+
// The SINGLETON half — standalone, agent-dev owns the MCP server (in cluster
|
|
174
|
+
// mode the gateway owns it instead; a `lens:true` app must NOT spawn its own).
|
|
149
175
|
if (mcpBundle) {
|
|
150
176
|
console.log(`[agent-dev] starting LensMCP MCP server (${mcpBundle})`);
|
|
151
177
|
spawnChild('lensmcp-mcp', process.execPath, [mcpBundle], {
|
|
@@ -154,32 +180,6 @@ async function agentDevExecutor(options, context) {
|
|
|
154
180
|
LENSMCP_PORT: process.env['LENSMCP_PORT'] ?? '3000',
|
|
155
181
|
});
|
|
156
182
|
}
|
|
157
|
-
if (opts.chrome) {
|
|
158
|
-
// Live browser capture: launch a headless Chrome against the dev
|
|
159
|
-
// URL, forward real console/network/exception events + DOMSnapshot
|
|
160
|
-
// visual frames into the shared event file (best-effort — skips
|
|
161
|
-
// cleanly if no Chrome). Spawned as an optional child so its exit
|
|
162
|
-
// never tears down Vite/MCP.
|
|
163
|
-
const captureRunner = findCaptureRunner(context.root);
|
|
164
|
-
const devUrl = opts.openUrl ?? (devPort ? `http://localhost:${devPort}/` : undefined);
|
|
165
|
-
if (!captureRunner) {
|
|
166
|
-
console.warn('[agent-dev] browser-capture runner not built — skipping live capture.');
|
|
167
|
-
}
|
|
168
|
-
else if (!devUrl) {
|
|
169
|
-
console.warn('[agent-dev] no dev URL/port resolved — skipping live capture.');
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
console.log(`[agent-dev] starting browser capture → ${devUrl}`);
|
|
173
|
-
spawnChild('browser-capture', process.execPath, [captureRunner], {
|
|
174
|
-
...bridgeEnv,
|
|
175
|
-
LENSMCP_DEV_URL: devUrl,
|
|
176
|
-
LENSMCP_TOKENS_FILE: (0, node_path_1.join)(context.root, 'lensmcp.tokens.json'),
|
|
177
|
-
LENSMCP_RULES_FILE: (0, node_path_1.join)(context.root, 'lensmcp.rules.json'),
|
|
178
|
-
LENSMCP_HEADLESS: opts.headless === false ? 'false' : 'true',
|
|
179
|
-
}, true, // optional
|
|
180
|
-
{ delayMs: 10_000, max: 5 });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
183
|
// Wait until any child exits.
|
|
184
184
|
return new Promise((res) => {
|
|
185
185
|
const tick = () => {
|
|
@@ -248,80 +248,178 @@ async function runNestjs(projectRoot, context) {
|
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
250
|
}
|
|
251
|
-
// ----------
|
|
252
|
-
function locateBin(name, roots) {
|
|
253
|
-
for (const root of roots) {
|
|
254
|
-
const candidate = (0, node_path_1.join)(root, 'node_modules', '.bin', name);
|
|
255
|
-
if ((0, node_fs_1.existsSync)(candidate))
|
|
256
|
-
return candidate;
|
|
257
|
-
}
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
function resolveFirstExisting(names, root) {
|
|
261
|
-
for (const n of names) {
|
|
262
|
-
const p = (0, node_path_1.join)(root, n);
|
|
263
|
-
if ((0, node_fs_1.existsSync)(p))
|
|
264
|
-
return p;
|
|
265
|
-
}
|
|
266
|
-
return undefined;
|
|
267
|
-
}
|
|
251
|
+
// ---------- imported / bring-your-own-build branch ----------
|
|
268
252
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
253
|
+
* `kind: "imported"` — for hosts LensMCP does NOT build: webpack, Next.js, a
|
|
254
|
+
* custom builder, or no build at all. Instead of owning the dev server, we run
|
|
255
|
+
* the host's OWN dev command and wrap it with the build-agnostic seams:
|
|
256
|
+
*
|
|
257
|
+
* 1. the standalone bridge sidecar (`@lensmcp/bridge`) on a FIXED ws port,
|
|
258
|
+
* so the injected `@lensmcp/client-runtime` finds it by a known URL;
|
|
259
|
+
* 2. the LensMCP MCP server (tails the shared event file);
|
|
260
|
+
* 3. the host's `devCommand`, with `LENSMCP_EVENT_FILE` + `LENSMCP_WS_PORT`
|
|
261
|
+
* exported and (when installed) `NODE_OPTIONS=--import
|
|
262
|
+
* @lensmcp/node-instrumentation/register` for zero-touch backend taps;
|
|
263
|
+
* 4. an optional Chrome capture sidecar against `openUrl`.
|
|
264
|
+
*
|
|
265
|
+
* The frontend seam is the user's responsibility (one line —
|
|
266
|
+
* `import '@lensmcp/client-runtime/auto'`), since we don't control their build.
|
|
273
267
|
*/
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
268
|
+
async function runImported(projectRoot, context, opts) {
|
|
269
|
+
if (!opts.devCommand) {
|
|
270
|
+
console.error('[agent-dev] kind="imported" needs a `devCommand` (your dev-server command, e.g. "npm run dev").\n' +
|
|
271
|
+
' Add it to the agent-dev target options, or pass `--devCommand "npm run dev"`.');
|
|
272
|
+
return { success: false };
|
|
273
|
+
}
|
|
274
|
+
const bridgeBundle = (0, lens_frontend_1.findBridgeBundle)(context.root);
|
|
275
|
+
if (!bridgeBundle) {
|
|
276
|
+
console.error("[agent-dev] No lensmcp bridge bundle found. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/bridge.js`).");
|
|
277
|
+
return { success: false };
|
|
278
|
+
}
|
|
279
|
+
const mcpBundle = (0, lens_frontend_1.findMcpBundle)(context.root);
|
|
280
|
+
if (!mcpBundle) {
|
|
281
|
+
console.warn("[agent-dev] No lensmcp-mcp bundle found — skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp`.");
|
|
282
|
+
}
|
|
283
|
+
const children = [];
|
|
284
|
+
const respawnCounts = new Map();
|
|
285
|
+
let exitRequested = false;
|
|
286
|
+
function spawnChild(label, bin, args, env, optional = false, respawn, useShell = false) {
|
|
287
|
+
const child = (0, node_child_process_1.spawn)(bin, args, {
|
|
288
|
+
cwd: projectRoot,
|
|
289
|
+
env: { ...process.env, ...(env ?? {}) },
|
|
290
|
+
stdio: 'inherit',
|
|
291
|
+
shell: useShell,
|
|
292
|
+
});
|
|
293
|
+
children.push(child);
|
|
294
|
+
child.on('exit', (code, sig) => {
|
|
295
|
+
if (exitRequested)
|
|
296
|
+
return;
|
|
297
|
+
if (optional) {
|
|
298
|
+
const used = respawnCounts.get(label) ?? 0;
|
|
299
|
+
if (respawn && used < respawn.max) {
|
|
300
|
+
respawnCounts.set(label, used + 1);
|
|
301
|
+
console.warn(`[agent-dev] ${label} exited (code=${code} sig=${sig}); respawning in ${Math.round(respawn.delayMs / 1000)}s (${used + 1}/${respawn.max}).`);
|
|
302
|
+
const t = setTimeout(() => {
|
|
303
|
+
if (!exitRequested)
|
|
304
|
+
spawnChild(label, bin, args, env, optional, respawn, useShell);
|
|
305
|
+
}, respawn.delayMs);
|
|
306
|
+
t.unref?.();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.warn(`[agent-dev] ${label} exited (code=${code} sig=${sig}); continuing.`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.warn(`[agent-dev] ${label} exited unexpectedly (code=${code} sig=${sig}); shutting down others.`);
|
|
313
|
+
shutdown();
|
|
314
|
+
});
|
|
315
|
+
return child;
|
|
316
|
+
}
|
|
317
|
+
function shutdown() {
|
|
318
|
+
if (exitRequested)
|
|
319
|
+
return;
|
|
320
|
+
exitRequested = true;
|
|
321
|
+
for (const c of children) {
|
|
322
|
+
try {
|
|
323
|
+
c.kill('SIGTERM');
|
|
324
|
+
}
|
|
325
|
+
catch { /* swallow */ }
|
|
326
|
+
}
|
|
282
327
|
}
|
|
328
|
+
process.on('SIGINT', shutdown);
|
|
329
|
+
process.on('SIGTERM', shutdown);
|
|
330
|
+
// Shared event bridge file (same contract as the Vite branch).
|
|
331
|
+
const eventFile = process.env['LENSMCP_EVENT_FILE'] ?? (0, node_path_1.join)(context.root, '.lensmcp', 'events.jsonl');
|
|
283
332
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
333
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(eventFile), { recursive: true });
|
|
334
|
+
const st = (0, node_fs_1.statSync)(eventFile, { throwIfNoEntry: false });
|
|
335
|
+
if (!st || Date.now() - st.mtimeMs > 60_000) {
|
|
336
|
+
(0, node_fs_1.writeFileSync)(eventFile, '');
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
console.log('[agent-dev] event bridge is active (written <60s ago) — appending, not truncating.');
|
|
340
|
+
}
|
|
286
341
|
}
|
|
287
342
|
catch {
|
|
288
|
-
|
|
343
|
+
/* non-fatal — ingest tolerates a missing file */
|
|
289
344
|
}
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
345
|
+
const bridgeEnv = { LENSMCP_EVENT_FILE: eventFile };
|
|
346
|
+
console.log(`[agent-dev] event bridge → ${eventFile}`);
|
|
347
|
+
// 1. Standalone browser-event bridge on a fixed port.
|
|
348
|
+
console.log(`[agent-dev] starting bridge → ws://127.0.0.1:${opts.wsPort}`);
|
|
349
|
+
spawnChild('bridge', process.execPath, [bridgeBundle], {
|
|
350
|
+
...bridgeEnv,
|
|
351
|
+
LENSMCP_WS_HOST: process.env['LENSMCP_WS_HOST'] ?? '127.0.0.1',
|
|
352
|
+
LENSMCP_WS_PORT: String(opts.wsPort),
|
|
353
|
+
});
|
|
354
|
+
// 2. MCP server.
|
|
355
|
+
if (mcpBundle) {
|
|
356
|
+
console.log(`[agent-dev] starting LensMCP MCP server (${mcpBundle})`);
|
|
357
|
+
spawnChild('lensmcp-mcp', process.execPath, [mcpBundle], {
|
|
358
|
+
...bridgeEnv,
|
|
359
|
+
LENSMCP_TRANSPORT: process.env['LENSMCP_TRANSPORT'] ?? 'http',
|
|
360
|
+
LENSMCP_PORT: process.env['LENSMCP_PORT'] ?? '3000',
|
|
361
|
+
});
|
|
307
362
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
363
|
+
// 3. The host's own dev command, wrapped with the event bridge + ws port,
|
|
364
|
+
// plus zero-touch backend taps when @lensmcp/node-instrumentation is
|
|
365
|
+
// installed (it's a no-op import otherwise, so we gate on presence).
|
|
366
|
+
const devEnv = { ...bridgeEnv, LENSMCP_WS_PORT: String(opts.wsPort) };
|
|
367
|
+
if (opts.node) {
|
|
368
|
+
const hasNodeInstr = [projectRoot, context.root].some((r) => (0, node_fs_1.existsSync)((0, node_path_1.join)(r, 'node_modules', '@lensmcp', 'node-instrumentation')));
|
|
369
|
+
if (hasNodeInstr) {
|
|
370
|
+
const register = '--import @lensmcp/node-instrumentation/register';
|
|
371
|
+
devEnv.NODE_OPTIONS = process.env['NODE_OPTIONS']
|
|
372
|
+
? `${process.env['NODE_OPTIONS']} ${register}`
|
|
373
|
+
: register;
|
|
374
|
+
console.log('[agent-dev] backend taps: NODE_OPTIONS += --import @lensmcp/node-instrumentation/register');
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.warn('[agent-dev] @lensmcp/node-instrumentation not installed — skipping backend taps. Add it: `npm i -D @lensmcp/node-instrumentation`.');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
console.log(`[agent-dev] front-end seam: add \`import '@lensmcp/client-runtime/auto'\` to your app entry (connects to ws://127.0.0.1:${opts.wsPort}).`);
|
|
381
|
+
console.log(`[agent-dev] starting dev command: ${opts.devCommand}`);
|
|
382
|
+
spawnChild('dev', opts.devCommand, [], devEnv, false, undefined, true /* shell */);
|
|
383
|
+
// 4. Optional Chrome capture (needs a known URL — we don't infer the port).
|
|
384
|
+
if (opts.chrome) {
|
|
385
|
+
const captureRunner = (0, lens_frontend_1.findCaptureRunner)(context.root);
|
|
386
|
+
const devUrl = opts.openUrl;
|
|
387
|
+
if (!captureRunner) {
|
|
388
|
+
console.warn("[agent-dev] browser-capture runner not found — skipping live capture. Install the self-contained CLI: `npm i -D lensmcp`.");
|
|
389
|
+
}
|
|
390
|
+
else if (!devUrl) {
|
|
391
|
+
console.warn('[agent-dev] no `openUrl` provided — skipping Chrome capture (set openUrl to your dev URL).');
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
console.log(`[agent-dev] starting browser capture → ${devUrl}`);
|
|
395
|
+
spawnChild('browser-capture', process.execPath, [captureRunner], {
|
|
396
|
+
...bridgeEnv,
|
|
397
|
+
LENSMCP_DEV_URL: devUrl,
|
|
398
|
+
LENSMCP_TOKENS_FILE: (0, node_path_1.join)(context.root, 'lensmcp.tokens.json'),
|
|
399
|
+
LENSMCP_RULES_FILE: (0, node_path_1.join)(context.root, 'lensmcp.rules.json'),
|
|
400
|
+
LENSMCP_HEADLESS: opts.headless === false ? 'false' : 'true',
|
|
401
|
+
}, true, { delayMs: 10_000, max: 5 });
|
|
402
|
+
}
|
|
325
403
|
}
|
|
326
|
-
|
|
404
|
+
// Wait until any non-optional child exits.
|
|
405
|
+
return new Promise((res) => {
|
|
406
|
+
const tick = () => {
|
|
407
|
+
if (exitRequested) {
|
|
408
|
+
Promise.allSettled(children.map((c) => new Promise((r) => {
|
|
409
|
+
if (c.exitCode !== null)
|
|
410
|
+
r();
|
|
411
|
+
else
|
|
412
|
+
c.on('exit', () => r());
|
|
413
|
+
}))).then(() => res({ success: true }));
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
setTimeout(tick, 500);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
tick();
|
|
420
|
+
});
|
|
327
421
|
}
|
|
422
|
+
// ---------- helpers ----------
|
|
423
|
+
// The bundle/bin resolvers (locateBin, findMcpBundle, findCaptureRunner,
|
|
424
|
+
// findBridgeBundle, …) now live in the shared `../../lens-frontend` module so
|
|
425
|
+
// the cluster gateway reuses the EXACT same resolution. Imported at the top.
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export interface AgentDevExecutorSchema {
|
|
2
|
-
kind?: 'vite-react' | 'nestjs' | 'nextjs';
|
|
2
|
+
kind?: 'vite-react' | 'nestjs' | 'nextjs' | 'imported';
|
|
3
3
|
devTarget?: string;
|
|
4
|
+
/** For kind "imported": the host's own dev-server shell command. */
|
|
5
|
+
devCommand?: string;
|
|
4
6
|
projectRoot?: string;
|
|
5
7
|
chrome?: boolean;
|
|
6
8
|
headless?: boolean;
|
|
7
9
|
port?: number;
|
|
8
10
|
openUrl?: string;
|
|
11
|
+
/** For kind "imported": fixed bridge WS port the client runtime connects to. */
|
|
12
|
+
wsPort?: number;
|
|
13
|
+
/** For kind "imported": inject the node-instrumentation --import. Default true. */
|
|
14
|
+
node?: boolean;
|
|
9
15
|
}
|
|
@@ -8,13 +8,27 @@
|
|
|
8
8
|
"properties": {
|
|
9
9
|
"kind": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"enum": ["vite-react", "nestjs", "nextjs"],
|
|
11
|
+
"enum": ["vite-react", "nestjs", "nextjs", "imported"],
|
|
12
12
|
"default": "vite-react"
|
|
13
13
|
},
|
|
14
14
|
"devTarget": {
|
|
15
15
|
"type": "string",
|
|
16
16
|
"description": "Nx target that starts the host's dev server (e.g. \"web:serve\"). If omitted, the executor runs `vite` on the current project."
|
|
17
17
|
},
|
|
18
|
+
"devCommand": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "For kind \"imported\": the shell command that starts the host's own dev server (e.g. \"npm run dev\" or \"webpack serve\"). Run with LENSMCP_EVENT_FILE + LENSMCP_WS_PORT exported and (when @lensmcp/node-instrumentation is installed) NODE_OPTIONS=--import for backend taps."
|
|
21
|
+
},
|
|
22
|
+
"wsPort": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"default": 5747,
|
|
25
|
+
"description": "For kind \"imported\": fixed port the standalone bridge listens on. The injected @lensmcp/client-runtime connects here (default 5747)."
|
|
26
|
+
},
|
|
27
|
+
"node": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"default": true,
|
|
30
|
+
"description": "For kind \"imported\": inject NODE_OPTIONS=--import @lensmcp/node-instrumentation/register into the dev command so backend taps install with no code change. Skipped automatically if the package isn't installed."
|
|
31
|
+
},
|
|
18
32
|
"projectRoot": {
|
|
19
33
|
"type": "string",
|
|
20
34
|
"description": "Override the project root passed to Vite. Defaults to the running project's root."
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
/** A caller-supplied process spawner so spawned children join the caller's
|
|
3
|
+
* lifecycle (teardown, respawn policy, stdio). Mirrors the `spawnChild`
|
|
4
|
+
* closures inside `agent-dev` and the gateway. */
|
|
5
|
+
export type SpawnChild = (label: string, bin: string, args: string[], env?: NodeJS.ProcessEnv, optional?: boolean, respawn?: {
|
|
6
|
+
delayMs: number;
|
|
7
|
+
max: number;
|
|
8
|
+
}) => ChildProcess;
|
|
9
|
+
export interface LensFrontendOptions {
|
|
10
|
+
/** Absolute path to the frontend project root (holds vite.config.*). */
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
/** Workspace root — used to resolve bundles + the default event file. */
|
|
13
|
+
workspaceRoot: string;
|
|
14
|
+
/** Label for logs (the nx project name). */
|
|
15
|
+
project?: string;
|
|
16
|
+
/** Shared JSONL event bus the vite plugin + capture runner append to. */
|
|
17
|
+
eventFile: string;
|
|
18
|
+
/** Pin the Vite dev port (so the gateway upstream + capture URL are
|
|
19
|
+
* deterministic). Default 5173 when chrome capture is on, else Vite's own. */
|
|
20
|
+
port?: number;
|
|
21
|
+
/** Launch the headless browser-capture sidecar against the dev URL. */
|
|
22
|
+
chrome?: boolean;
|
|
23
|
+
/** Headless Chrome (default true). */
|
|
24
|
+
headless?: boolean;
|
|
25
|
+
/** Override the URL Chrome navigates to (default http://localhost:<port>/). */
|
|
26
|
+
openUrl?: string;
|
|
27
|
+
/** Override the vite binary path (else resolved from node_modules/.bin). */
|
|
28
|
+
viteBin?: string;
|
|
29
|
+
/** Extra env merged into the Vite child (e.g. cluster identity). */
|
|
30
|
+
viteEnv?: NodeJS.ProcessEnv;
|
|
31
|
+
/** Logger (default console.log). */
|
|
32
|
+
log?: (line: string) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface LensFrontendResult {
|
|
35
|
+
/** The pinned Vite dev port (undefined when unpinned). */
|
|
36
|
+
devPort?: number;
|
|
37
|
+
/** The dev URL the gateway should upstream to / Chrome captures. */
|
|
38
|
+
devUrl?: string;
|
|
39
|
+
/** The spawned Vite child. */
|
|
40
|
+
vite: ChildProcess;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Spawn ONE frontend project under the lens: its Vite dev server (lens plugin
|
|
44
|
+
* already wired by `setup-vite`) + an optional browser-capture sidecar, both
|
|
45
|
+
* publishing to the shared event bus. Does NOT spawn the dashboard or MCP
|
|
46
|
+
* server — those are singletons the CALLER owns.
|
|
47
|
+
*
|
|
48
|
+
* Returns the resolved dev port/URL so the gateway can route its TCP upstream
|
|
49
|
+
* at it. Throws only if Vite can't be located; capture is best-effort.
|
|
50
|
+
*/
|
|
51
|
+
export declare function spawnLensFrontend(options: LensFrontendOptions, spawnChild: SpawnChild): LensFrontendResult;
|
|
52
|
+
export declare function locateBin(name: string, roots: string[]): string | undefined;
|
|
53
|
+
export declare function resolveFirstExisting(names: string[], root: string): string | undefined;
|
|
54
|
+
export declare function globalNodeModules(): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the LensMCP MCP server bundle — the self-contained `lensmcp` CLI
|
|
57
|
+
* bundle (`bundled/main.js`), else the in-repo dev build. See the doc on
|
|
58
|
+
* `findCaptureRunner` for why we never reach into an unpublished `@lensmcp/*`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findMcpBundle(workspaceRoot: string): string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the lens DASHBOARD bundle — the human web view (`bundled/dashboard.js`,
|
|
63
|
+
* default :4321). Same shipping story as the MCP bundle.
|
|
64
|
+
*/
|
|
65
|
+
export declare function findDashboardBundle(workspaceRoot: string): string | undefined;
|
|
66
|
+
export declare function findCaptureRunner(workspaceRoot: string): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the standalone browser-event bridge runner — the self-contained
|
|
69
|
+
* `lensmcp` package's `bundled/bridge.js`, else the in-repo dev build.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findBridgeBundle(workspaceRoot: string): string | undefined;
|
|
72
|
+
//# sourceMappingURL=lens-frontend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lens-frontend.d.ts","sourceRoot":"","sources":["../src/lens-frontend.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;mDAEmD;AACnD,MAAM,MAAM,UAAU,GAAG,CACvB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,OAAO,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KACvC,YAAY,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB;mFAC+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC5B,oCAAoC;IACpC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,IAAI,EAAE,YAAY,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,EAC5B,UAAU,EAAE,UAAU,GACrB,kBAAkB,CAiEpB;AAID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAM3E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMtF;AASD,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CActD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ7E;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ1E"}
|
package/lens-frontend.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spawnLensFrontend = spawnLensFrontend;
|
|
4
|
+
exports.locateBin = locateBin;
|
|
5
|
+
exports.resolveFirstExisting = resolveFirstExisting;
|
|
6
|
+
exports.globalNodeModules = globalNodeModules;
|
|
7
|
+
exports.findMcpBundle = findMcpBundle;
|
|
8
|
+
exports.findDashboardBundle = findDashboardBundle;
|
|
9
|
+
exports.findCaptureRunner = findCaptureRunner;
|
|
10
|
+
exports.findBridgeBundle = findBridgeBundle;
|
|
11
|
+
/**
|
|
12
|
+
* Shared "run ONE frontend project under the lens" machinery.
|
|
13
|
+
*
|
|
14
|
+
* The SAME logic is needed in two places:
|
|
15
|
+
*
|
|
16
|
+
* 1. `agent-dev` (standalone, per-app) — `nx run web:agent-dev`. There it ALSO
|
|
17
|
+
* spawns the cluster-wide singletons (the MCP server + the dashboard), since
|
|
18
|
+
* nothing else owns them.
|
|
19
|
+
* 2. `@lensmcp/cluster`'s gateway (`nx run gateway:serve`, `lens: true` apps).
|
|
20
|
+
* There the gateway owns the singletons (ONE dashboard + ONE MCP server
|
|
21
|
+
* cluster-wide), so a `lens:true` app must run ONLY its Vite + the lens
|
|
22
|
+
* instrumentation + browser capture — never its own dashboard/MCP (they'd
|
|
23
|
+
* collide on :4321 / :3000).
|
|
24
|
+
*
|
|
25
|
+
* This module factors out the per-app half — Vite (with `@lensmcp/vite-plugin`
|
|
26
|
+
* already wired in the host's vite.config) + the optional browser-capture
|
|
27
|
+
* runner — plus the bundle/bin resolvers both callers share. The singleton half
|
|
28
|
+
* (dashboard + MCP) stays with each caller, because WHO owns the singletons is
|
|
29
|
+
* exactly what differs between the two.
|
|
30
|
+
*
|
|
31
|
+
* No `@nx/devkit` import here on purpose: the cluster gateway calls this from a
|
|
32
|
+
* plain runtime context, so the helper takes only POJOs + a `spawnChild`
|
|
33
|
+
* callback the caller supplies (so children join the caller's lifecycle/teardown).
|
|
34
|
+
*/
|
|
35
|
+
const node_child_process_1 = require("node:child_process");
|
|
36
|
+
const node_fs_1 = require("node:fs");
|
|
37
|
+
const node_path_1 = require("node:path");
|
|
38
|
+
/**
|
|
39
|
+
* Spawn ONE frontend project under the lens: its Vite dev server (lens plugin
|
|
40
|
+
* already wired by `setup-vite`) + an optional browser-capture sidecar, both
|
|
41
|
+
* publishing to the shared event bus. Does NOT spawn the dashboard or MCP
|
|
42
|
+
* server — those are singletons the CALLER owns.
|
|
43
|
+
*
|
|
44
|
+
* Returns the resolved dev port/URL so the gateway can route its TCP upstream
|
|
45
|
+
* at it. Throws only if Vite can't be located; capture is best-effort.
|
|
46
|
+
*/
|
|
47
|
+
function spawnLensFrontend(options, spawnChild) {
|
|
48
|
+
const log = options.log ?? ((l) => console.log(l));
|
|
49
|
+
const { projectRoot, workspaceRoot, eventFile } = options;
|
|
50
|
+
const chrome = options.chrome !== false;
|
|
51
|
+
const headless = options.headless !== false;
|
|
52
|
+
const viteBin = options.viteBin ?? locateBin('vite', [projectRoot, workspaceRoot]);
|
|
53
|
+
if (!viteBin) {
|
|
54
|
+
throw new Error('[lens] Could not find the vite binary in node_modules/.bin.');
|
|
55
|
+
}
|
|
56
|
+
// Pin the dev port so the gateway upstream + the capture target are
|
|
57
|
+
// deterministic. When chrome capture is on we always fix it (strictPort).
|
|
58
|
+
const devPort = options.port ?? (chrome ? 5173 : undefined);
|
|
59
|
+
const viteArgs = [
|
|
60
|
+
'--config',
|
|
61
|
+
resolveFirstExisting(['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs', 'vite.config.cts', 'vite.config.cjs'], projectRoot) ?? (0, node_path_1.join)(projectRoot, 'vite.config.ts'),
|
|
62
|
+
projectRoot,
|
|
63
|
+
];
|
|
64
|
+
if (devPort !== undefined) {
|
|
65
|
+
viteArgs.push('--port', String(devPort), '--strictPort');
|
|
66
|
+
}
|
|
67
|
+
const bridgeEnv = { LENSMCP_EVENT_FILE: eventFile, ...(options.viteEnv ?? {}) };
|
|
68
|
+
log(`[lens] starting Vite for ${options.project ?? projectRoot}${devPort ? ` on :${devPort}` : ''}`);
|
|
69
|
+
const vite = spawnChild('vite', viteBin, viteArgs, bridgeEnv);
|
|
70
|
+
const devUrl = options.openUrl ?? (devPort ? `http://localhost:${devPort}/` : undefined);
|
|
71
|
+
if (chrome) {
|
|
72
|
+
const captureRunner = findCaptureRunner(workspaceRoot);
|
|
73
|
+
if (!captureRunner) {
|
|
74
|
+
log("[lens] browser-capture runner not found — skipping live capture. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/capture-runner.js`).");
|
|
75
|
+
}
|
|
76
|
+
else if (!devUrl) {
|
|
77
|
+
log('[lens] no dev URL/port resolved — skipping live capture.');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
log(`[lens] starting browser capture → ${devUrl}`);
|
|
81
|
+
spawnChild('browser-capture', process.execPath, [captureRunner], {
|
|
82
|
+
...bridgeEnv,
|
|
83
|
+
LENSMCP_DEV_URL: devUrl,
|
|
84
|
+
LENSMCP_TOKENS_FILE: (0, node_path_1.join)(workspaceRoot, 'lensmcp.tokens.json'),
|
|
85
|
+
LENSMCP_RULES_FILE: (0, node_path_1.join)(workspaceRoot, 'lensmcp.rules.json'),
|
|
86
|
+
LENSMCP_HEADLESS: headless === false ? 'false' : 'true',
|
|
87
|
+
}, true, // optional — its exit never tears down the session
|
|
88
|
+
{ delayMs: 10_000, max: 5 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...(devPort !== undefined ? { devPort } : {}),
|
|
93
|
+
...(devUrl ? { devUrl } : {}),
|
|
94
|
+
vite,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// ---------- bundle / bin resolvers (shared by both callers) ----------
|
|
98
|
+
function locateBin(name, roots) {
|
|
99
|
+
for (const root of roots) {
|
|
100
|
+
const candidate = (0, node_path_1.join)(root, 'node_modules', '.bin', name);
|
|
101
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
function resolveFirstExisting(names, root) {
|
|
107
|
+
for (const n of names) {
|
|
108
|
+
const p = (0, node_path_1.join)(root, n);
|
|
109
|
+
if ((0, node_fs_1.existsSync)(p))
|
|
110
|
+
return p;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The global npm root — `npm i -g lensmcp` puts the bundled artefacts there,
|
|
116
|
+
* not in the host workspace. Fast path derives it from the running node binary
|
|
117
|
+
* (nvm/standard unix layout); `npm root -g` is the fallback. Cached for the
|
|
118
|
+
* process lifetime.
|
|
119
|
+
*/
|
|
120
|
+
let cachedGlobalRoot;
|
|
121
|
+
function globalNodeModules() {
|
|
122
|
+
if (cachedGlobalRoot !== undefined)
|
|
123
|
+
return cachedGlobalRoot ?? undefined;
|
|
124
|
+
const guess = (0, node_path_1.resolve)((0, node_path_1.dirname)(process.execPath), '..', 'lib', 'node_modules');
|
|
125
|
+
if ((0, node_fs_1.existsSync)(guess)) {
|
|
126
|
+
cachedGlobalRoot = guess;
|
|
127
|
+
return guess;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const out = (0, node_child_process_1.execSync)('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
131
|
+
cachedGlobalRoot = out && (0, node_fs_1.existsSync)(out) ? out : null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
cachedGlobalRoot = null;
|
|
135
|
+
}
|
|
136
|
+
return cachedGlobalRoot ?? undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the LensMCP MCP server bundle — the self-contained `lensmcp` CLI
|
|
140
|
+
* bundle (`bundled/main.js`), else the in-repo dev build. See the doc on
|
|
141
|
+
* `findCaptureRunner` for why we never reach into an unpublished `@lensmcp/*`.
|
|
142
|
+
*/
|
|
143
|
+
function findMcpBundle(workspaceRoot) {
|
|
144
|
+
const globalRoot = globalNodeModules();
|
|
145
|
+
return firstExisting([
|
|
146
|
+
(0, node_path_1.join)(workspaceRoot, 'node_modules', 'lensmcp', 'bundled', 'main.js'),
|
|
147
|
+
...(globalRoot ? [(0, node_path_1.join)(globalRoot, 'lensmcp', 'bundled', 'main.js')] : []),
|
|
148
|
+
// In-repo dev build (four-bucket layout) — never in a host's node_modules.
|
|
149
|
+
(0, node_path_1.join)(workspaceRoot, 'servers', 'lensmcp-mcp', 'dist', 'main.js'),
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the lens DASHBOARD bundle — the human web view (`bundled/dashboard.js`,
|
|
154
|
+
* default :4321). Same shipping story as the MCP bundle.
|
|
155
|
+
*/
|
|
156
|
+
function findDashboardBundle(workspaceRoot) {
|
|
157
|
+
const globalRoot = globalNodeModules();
|
|
158
|
+
return firstExisting([
|
|
159
|
+
(0, node_path_1.join)(workspaceRoot, 'node_modules', 'lensmcp', 'bundled', 'dashboard.js'),
|
|
160
|
+
...(globalRoot ? [(0, node_path_1.join)(globalRoot, 'lensmcp', 'bundled', 'dashboard.js')] : []),
|
|
161
|
+
// In-repo dev build — never in a host's node_modules.
|
|
162
|
+
(0, node_path_1.join)(workspaceRoot, 'servers', 'lensmcp-mcp', 'dist', 'dashboard.js'),
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
function findCaptureRunner(workspaceRoot) {
|
|
166
|
+
const globalRoot = globalNodeModules();
|
|
167
|
+
return firstExisting([
|
|
168
|
+
(0, node_path_1.join)(workspaceRoot, 'node_modules', 'lensmcp', 'bundled', 'capture-runner.js'),
|
|
169
|
+
...(globalRoot ? [(0, node_path_1.join)(globalRoot, 'lensmcp', 'bundled', 'capture-runner.js')] : []),
|
|
170
|
+
// In-repo dev build — never in a host's node_modules.
|
|
171
|
+
(0, node_path_1.join)(workspaceRoot, 'libs', 'browser-capture', 'dist', 'capture-runner.js'),
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the standalone browser-event bridge runner — the self-contained
|
|
176
|
+
* `lensmcp` package's `bundled/bridge.js`, else the in-repo dev build.
|
|
177
|
+
*/
|
|
178
|
+
function findBridgeBundle(workspaceRoot) {
|
|
179
|
+
const globalRoot = globalNodeModules();
|
|
180
|
+
return firstExisting([
|
|
181
|
+
(0, node_path_1.join)(workspaceRoot, 'node_modules', 'lensmcp', 'bundled', 'bridge.js'),
|
|
182
|
+
...(globalRoot ? [(0, node_path_1.join)(globalRoot, 'lensmcp', 'bundled', 'bridge.js')] : []),
|
|
183
|
+
// In-repo dev build — never in a host's node_modules.
|
|
184
|
+
(0, node_path_1.join)(workspaceRoot, 'libs', 'bridge', 'dist', 'main.js'),
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
function firstExisting(candidates) {
|
|
188
|
+
for (const c of candidates) {
|
|
189
|
+
if ((0, node_fs_1.existsSync)(c))
|
|
190
|
+
return c;
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/nx-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./index.d.ts",
|
|
11
11
|
"import": "./index.js",
|
|
12
12
|
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./lens-frontend": {
|
|
15
|
+
"types": "./lens-frontend.d.ts",
|
|
16
|
+
"import": "./lens-frontend.js",
|
|
17
|
+
"default": "./lens-frontend.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"dependencies": {
|