@lensmcp/nx-plugin 1.0.0 → 1.2.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.
@@ -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 via @lensmcp/bundle once the supervisor wires the plugin bus.',
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
- * Phase 1 keeps this minimal: spawn-and-supervise, no clever
16
- * inter-process correlation. The real session-supervisor coordinator
17
- * lands in Phase 2/3 as `@lensmcp/session` extensions.
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;AAClD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAiNzB"}
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
- * Phase 1 keeps this minimal: spawn-and-supervise, no clever
17
- * inter-process correlation. The real session-supervisor coordinator
18
- * lands in Phase 2/3 as `@lensmcp/session` extensions.
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] Phase 3 supports kind="vite-react" or "nestjs" (got "${opts.kind}").`);
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
- // ---- Vite/React branch (Phase 1) ----
48
- const viteBin = locateBin('vite', [projectRoot, context.root]);
49
- if (!viteBin) {
50
- console.error('[agent-dev] Could not find vite binary in node_modules/.bin.');
51
- return { success: false };
52
- }
53
- // Pin the dev port so the browser-capture driver knows the URL. When
54
- // Chrome capture is on we always fix it (strictPort) so the attach
55
- // target is deterministic.
56
- const devPort = opts.port ?? (opts.chrome ? 5173 : undefined);
57
- const viteArgs = [
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('[agent-dev] No lensmcp-mcp bundle found — skipping MCP server.');
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
- console.log(`[agent-dev] starting Vite for ${project ?? projectRoot}`);
148
- spawnChild('vite', viteBin, viteArgs, bridgeEnv);
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
- // ---------- helpers ----------
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
- * The global npm root the recommended `npm i -g lensmcp` puts the bundled
270
- * artefacts there, not in the host workspace. Fast path derives it from the
271
- * running node binary (nvm/standard unix layout); `npm root -g` is the
272
- * fallback. Cached for the executor's lifetime.
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
- let cachedGlobalRoot;
275
- function globalNodeModules() {
276
- if (cachedGlobalRoot !== undefined)
277
- return cachedGlobalRoot ?? undefined;
278
- const guess = (0, node_path_1.resolve)((0, node_path_1.dirname)(process.execPath), '..', 'lib', 'node_modules');
279
- if ((0, node_fs_1.existsSync)(guess)) {
280
- cachedGlobalRoot = guess;
281
- return guess;
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
- const out = (0, node_child_process_1.execSync)('npm root -g', { encoding: 'utf8', timeout: 5000 }).trim();
285
- cachedGlobalRoot = out && (0, node_fs_1.existsSync)(out) ? out : null;
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
- cachedGlobalRoot = null;
343
+ /* non-fatal — ingest tolerates a missing file */
289
344
  }
290
- return cachedGlobalRoot ?? undefined;
291
- }
292
- function findMcpBundle(workspaceRoot) {
293
- const globalRoot = globalNodeModules();
294
- const candidates = [
295
- // Published host install: the bundled server inside the `lensmcp` package.
296
- (0, node_path_1.join)(workspaceRoot, 'node_modules', 'lensmcp', 'bundled', 'main.js'),
297
- // Global CLI install (`npm i -g lensmcp`).
298
- ...(globalRoot ? [(0, node_path_1.join)(globalRoot, 'lensmcp', 'bundled', 'main.js')] : []),
299
- // Pre-rename layout (kept for old installs).
300
- (0, node_path_1.join)(workspaceRoot, 'node_modules', '@lensmcp', 'cli', 'bundled', 'main.js'),
301
- // In-repo dev build (four-bucket layout).
302
- (0, node_path_1.join)(workspaceRoot, 'servers', 'lensmcp-mcp', 'dist', 'main.js'),
303
- ];
304
- for (const c of candidates) {
305
- if ((0, node_fs_1.existsSync)(c))
306
- return c;
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
- return undefined;
309
- }
310
- function findCaptureRunner(workspaceRoot) {
311
- const globalRoot = globalNodeModules();
312
- const candidates = [
313
- // Published host install: bundled inside the `lensmcp` package (ship step).
314
- (0, node_path_1.join)(workspaceRoot, 'node_modules', 'lensmcp', 'bundled', 'capture-runner.js'),
315
- // Global CLI install (`npm i -g lensmcp`).
316
- ...(globalRoot ? [(0, node_path_1.join)(globalRoot, 'lensmcp', 'bundled', 'capture-runner.js')] : []),
317
- // Pre-bundling layout (kept for old installs).
318
- (0, node_path_1.join)(workspaceRoot, 'node_modules', '@lensmcp', 'browser-capture', 'dist', 'capture-runner.js'),
319
- // In-repo dev build.
320
- (0, node_path_1.join)(workspaceRoot, 'libs', 'browser-capture', 'dist', 'capture-runner.js'),
321
- ];
322
- for (const c of candidates) {
323
- if ((0, node_fs_1.existsSync)(c))
324
- return c;
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
- return undefined;
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."
@@ -1 +1 @@
1
- {"version":3,"file":"agent-verify.d.ts","sourceRoot":"","sources":["../../../src/executors/agent-verify/agent-verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAE1D,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAA8B,mBAAmB,CAC/C,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAyHzB"}
1
+ {"version":3,"file":"agent-verify.d.ts","sourceRoot":"","sources":["../../../src/executors/agent-verify/agent-verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAE1D,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAA8B,mBAAmB,CAC/C,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAgHzB"}
@@ -43,11 +43,7 @@ async function agentVerifyExecutor(options, context) {
43
43
  }
44
44
  else {
45
45
  const t0 = Date.now();
46
- console.log('[agent-verify] tsc --noEmit');
47
- const r = (0, node_child_process_1.spawnSync)(tscBin, ['--noEmit', '-p', tsconfig], {
48
- cwd: projectRoot,
49
- stdio: 'inherit',
50
- });
46
+ const r = runStageCommand('tsc --noEmit', tscBin, ['--noEmit', '-p', tsconfig], projectRoot);
51
47
  stages.push({
52
48
  name: 'typecheck',
53
49
  result: r.status === 0 ? 'passed' : 'failed',
@@ -67,8 +63,7 @@ async function agentVerifyExecutor(options, context) {
67
63
  }
68
64
  else {
69
65
  const t0 = Date.now();
70
- console.log('[agent-verify] eslint .');
71
- const r = (0, node_child_process_1.spawnSync)(eslintBin, ['.'], { cwd: projectRoot, stdio: 'inherit' });
66
+ const r = runStageCommand('eslint .', eslintBin, ['.'], projectRoot);
72
67
  stages.push({
73
68
  name: 'lint',
74
69
  result: r.status === 0 ? 'passed' : 'failed',
@@ -90,11 +85,7 @@ async function agentVerifyExecutor(options, context) {
90
85
  const viteConfig = resolveFirstExisting(['vite.config.ts', 'vite.config.js', 'vite.config.mjs'], projectRoot) ??
91
86
  (0, node_path_1.join)(projectRoot, 'vite.config.ts');
92
87
  const t0 = Date.now();
93
- console.log('[agent-verify] vite build');
94
- const r = (0, node_child_process_1.spawnSync)(viteBin, ['build', '--config', viteConfig, projectRoot], {
95
- cwd: projectRoot,
96
- stdio: 'inherit',
97
- });
88
+ const r = runStageCommand('vite build', viteBin, ['build', '--config', viteConfig, projectRoot], projectRoot);
98
89
  stages.push({
99
90
  name: 'build',
100
91
  result: r.status === 0 ? 'passed' : 'failed',
@@ -135,6 +126,25 @@ async function agentVerifyExecutor(options, context) {
135
126
  console.log(`[agent-verify] ${status} in ${report.durationMs}ms`);
136
127
  return { success: status === 'passed' };
137
128
  }
129
+ /**
130
+ * Run a verify sub-command, capturing its output and re-emitting it INDENTED.
131
+ *
132
+ * agent-verify deliberately runs commands that may fail — it IS the agent's
133
+ * edit→verify loop, so a failing stage is a normal, expected outcome. In CI a
134
+ * raw tool error printed at column 0 (`file(line,col): error TS....`) is picked
135
+ * up by GitHub's problem matchers and turned into a spurious build *annotation*.
136
+ * Indenting every line by two spaces keeps the output fully readable while
137
+ * defeating the matchers (they anchor on a non-space first character), so a
138
+ * verify failure never masquerades as a CI error.
139
+ */
140
+ function runStageCommand(label, bin, args, cwd) {
141
+ console.log(`[agent-verify] ${label}`);
142
+ const r = (0, node_child_process_1.spawnSync)(bin, args, { cwd, encoding: 'utf8' });
143
+ const out = `${r.stdout ?? ''}${r.stderr ?? ''}`;
144
+ if (out.trim())
145
+ process.stdout.write(out.replace(/^(?=.)/gm, ' '));
146
+ return r;
147
+ }
138
148
  function locateBin(name, roots) {
139
149
  for (const root of roots) {
140
150
  const candidate = (0, node_path_1.join)(root, 'node_modules', '.bin', name);
package/executors.json CHANGED
@@ -4,18 +4,18 @@
4
4
  "version": "0.0.1",
5
5
  "executors": {
6
6
  "agent-dev": {
7
- "implementation": "./dist/executors/agent-dev/agent-dev",
8
- "schema": "./dist/executors/agent-dev/schema.json",
7
+ "implementation": "./executors/agent-dev/agent-dev",
8
+ "schema": "./executors/agent-dev/schema.json",
9
9
  "description": "Start a project with the LensMCP lens attached (Vite dev server + Chrome sidecar + MCP server)."
10
10
  },
11
11
  "agent-build": {
12
- "implementation": "./dist/executors/agent-build/agent-build",
13
- "schema": "./dist/executors/agent-build/schema.json",
12
+ "implementation": "./executors/agent-build/agent-build",
13
+ "schema": "./executors/agent-build/schema.json",
14
14
  "description": "Instrumented production build that produces a bundle report and updates the baseline."
15
15
  },
16
16
  "agent-verify": {
17
- "implementation": "./dist/executors/agent-verify/agent-verify",
18
- "schema": "./dist/executors/agent-verify/schema.json",
17
+ "implementation": "./executors/agent-verify/agent-verify",
18
+ "schema": "./executors/agent-verify/schema.json",
19
19
  "description": "Deterministic verification: typecheck → lint → build, with a per-run report under .lensmcp/verifications/."
20
20
  }
21
21
  }
package/generators.json CHANGED
@@ -4,18 +4,18 @@
4
4
  "version": "0.0.1",
5
5
  "generators": {
6
6
  "init": {
7
- "factory": "./dist/generators/init/init",
8
- "schema": "./dist/generators/init/schema.json",
7
+ "factory": "./generators/init/init",
8
+ "schema": "./generators/init/schema.json",
9
9
  "description": "Install LensMCP into a host Nx workspace (idempotent)."
10
10
  },
11
11
  "setup-vite": {
12
- "factory": "./dist/generators/setup-vite/setup-vite",
13
- "schema": "./dist/generators/setup-vite/schema.json",
12
+ "factory": "./generators/setup-vite/setup-vite",
13
+ "schema": "./generators/setup-vite/schema.json",
14
14
  "description": "Wire LensMCP into a Vite project (patches vite.config + adds agent-dev target)."
15
15
  },
16
16
  "setup-nest": {
17
- "factory": "./dist/generators/setup-nest/setup-nest",
18
- "schema": "./dist/generators/setup-nest/schema.json",
17
+ "factory": "./generators/setup-nest/setup-nest",
18
+ "schema": "./generators/setup-nest/schema.json",
19
19
  "description": "Wire LensMCP into a NestJS project (rewrites main.ts bootstrap to createLensmcpNestApp + agent-dev target)."
20
20
  }
21
21
  }
@@ -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"}
@@ -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.0.0",
3
+ "version": "1.2.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": {