@lensmcp/nx-plugin 1.18.4 → 1.18.7
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 +2 -86
- package/executors/agent-dev/agent-dev.js +4 -425
- package/executors/agent-verify/agent-verify.js +3 -166
- package/generators/init/init.js +9 -167
- package/generators/setup-nest/setup-nest.js +8 -281
- package/generators/setup-vite/setup-vite.js +5 -125
- package/index.js +1 -21
- package/lens-frontend.js +1 -205
- package/package.json +1 -1
|
@@ -1,86 +1,2 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
exports.default = agentBuildExecutor;
|
|
4
|
-
const node_fs_1 = require("node:fs");
|
|
5
|
-
const node_path_1 = require("node:path");
|
|
6
|
-
const node_child_process_1 = require("node:child_process");
|
|
7
|
-
const node_fs_2 = require("node:fs");
|
|
8
|
-
/**
|
|
9
|
-
* `agent-build` — one-shot production build of the host's Vite project,
|
|
10
|
-
* with `@lensmcp/vite-plugin` already wired (the `setup-vite` generator
|
|
11
|
-
* patches the host's `vite.config.ts`).
|
|
12
|
-
*
|
|
13
|
-
* Phase 2 keeps this minimal: spawn Vite's build CLI inheriting stdio,
|
|
14
|
-
* then (if the build succeeds and `updateBaseline` is true) write a
|
|
15
|
-
* stub baseline file under `.lensmcp/baseline/bundle.json` recording the
|
|
16
|
-
* timestamp + buildOutDir. The reducer-side baseline (per-chunk JSON)
|
|
17
|
-
* is written by the running `lensmcp-mcp` session — agent-build only
|
|
18
|
-
* spawns Vite; the report flows into MCP via the plugin's WS bridge
|
|
19
|
-
* once the supervisor wiring lands in 1.5.
|
|
20
|
-
*/
|
|
21
|
-
async function agentBuildExecutor(options, context) {
|
|
22
|
-
const opts = {
|
|
23
|
-
kind: options.kind ?? 'vite-react',
|
|
24
|
-
updateBaseline: options.updateBaseline ?? true,
|
|
25
|
-
projectRoot: options.projectRoot,
|
|
26
|
-
};
|
|
27
|
-
if (opts.kind !== 'vite-react') {
|
|
28
|
-
console.error(`[agent-build] Phase 2 only supports kind="vite-react" (got "${opts.kind}").`);
|
|
29
|
-
return { success: false };
|
|
30
|
-
}
|
|
31
|
-
const projectName = context.projectName;
|
|
32
|
-
const projectRoot = opts.projectRoot
|
|
33
|
-
? (0, node_path_1.resolve)(context.root, opts.projectRoot)
|
|
34
|
-
: projectName && context.projectsConfigurations
|
|
35
|
-
? (0, node_path_1.resolve)(context.root, context.projectsConfigurations.projects[projectName]?.root ?? '.')
|
|
36
|
-
: context.root;
|
|
37
|
-
const viteBin = locateBin('vite', [projectRoot, context.root]);
|
|
38
|
-
if (!viteBin) {
|
|
39
|
-
console.error('[agent-build] Could not find vite binary in node_modules/.bin.');
|
|
40
|
-
return { success: false };
|
|
41
|
-
}
|
|
42
|
-
const viteConfig = resolveFirstExisting(['vite.config.ts', 'vite.config.js', 'vite.config.mjs'], projectRoot) ??
|
|
43
|
-
(0, node_path_1.join)(projectRoot, 'vite.config.ts');
|
|
44
|
-
console.log(`[agent-build] vite build (project=${projectName ?? projectRoot})`);
|
|
45
|
-
const result = (0, node_child_process_1.spawnSync)(viteBin, ['build', '--config', viteConfig, projectRoot], {
|
|
46
|
-
cwd: projectRoot,
|
|
47
|
-
stdio: 'inherit',
|
|
48
|
-
env: { ...process.env },
|
|
49
|
-
});
|
|
50
|
-
const success = (result.status ?? 1) === 0;
|
|
51
|
-
if (success && opts.updateBaseline) {
|
|
52
|
-
const baselinePath = (0, node_path_1.join)(context.root, '.lensmcp', 'baseline', `${projectName ?? 'app'}.bundle.json`);
|
|
53
|
-
try {
|
|
54
|
-
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(baselinePath), { recursive: true });
|
|
55
|
-
(0, node_fs_1.writeFileSync)(baselinePath, JSON.stringify({
|
|
56
|
-
schemaVersion: 1,
|
|
57
|
-
project: projectName ?? null,
|
|
58
|
-
buildOutDir: (0, node_path_1.join)(projectRoot, 'dist'),
|
|
59
|
-
timestamp: new Date().toISOString(),
|
|
60
|
-
note: 'Phase 2 baseline marker. Per-chunk JSON is materialised by the running ' +
|
|
61
|
-
'lensmcp-mcp session (the self-contained bundle) once the supervisor wires the plugin bus.',
|
|
62
|
-
}, null, 2) + '\n');
|
|
63
|
-
console.log(`[agent-build] baseline written: ${baselinePath}`);
|
|
64
|
-
}
|
|
65
|
-
catch (e) {
|
|
66
|
-
console.warn(`[agent-build] could not write baseline: ${e.message}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return { success };
|
|
70
|
-
}
|
|
71
|
-
function locateBin(name, roots) {
|
|
72
|
-
for (const root of roots) {
|
|
73
|
-
const candidate = (0, node_path_1.join)(root, 'node_modules', '.bin', name);
|
|
74
|
-
if ((0, node_fs_2.existsSync)(candidate))
|
|
75
|
-
return candidate;
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
function resolveFirstExisting(names, root) {
|
|
80
|
-
for (const n of names) {
|
|
81
|
-
const p = (0, node_path_1.join)(root, n);
|
|
82
|
-
if ((0, node_fs_2.existsSync)(p))
|
|
83
|
-
return p;
|
|
84
|
-
}
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
1
|
+
"use strict";var f=Object.defineProperty;var r=(n,e)=>f(n,"name",{value:e,configurable:!0});var d=Object.defineProperty,i=r((n,e)=>d(n,"name",{value:e,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=agentBuildExecutor;const node_fs_1=require("node:fs"),node_path_1=require("node:path"),node_child_process_1=require("node:child_process"),node_fs_2=require("node:fs");async function agentBuildExecutor(n,e){const t={kind:n.kind??"vite-react",updateBaseline:n.updateBaseline??!0,projectRoot:n.projectRoot};if(t.kind!=="vite-react")return console.error(`[agent-build] Phase 2 only supports kind="vite-react" (got "${t.kind}").`),{success:!1};const o=e.projectName,s=t.projectRoot?(0,node_path_1.resolve)(e.root,t.projectRoot):o&&e.projectsConfigurations?(0,node_path_1.resolve)(e.root,e.projectsConfigurations.projects[o]?.root??"."):e.root,u=locateBin("vite",[s,e.root]);if(!u)return console.error("[agent-build] Could not find vite binary in node_modules/.bin."),{success:!1};const a=resolveFirstExisting(["vite.config.ts","vite.config.js","vite.config.mjs"],s)??(0,node_path_1.join)(s,"vite.config.ts");console.log(`[agent-build] vite build (project=${o??s})`);const l=((0,node_child_process_1.spawnSync)(u,["build","--config",a,s],{cwd:s,stdio:"inherit",env:{...process.env}}).status??1)===0;if(l&&t.updateBaseline){const c=(0,node_path_1.join)(e.root,".lensmcp","baseline",`${o??"app"}.bundle.json`);try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(c),{recursive:!0}),(0,node_fs_1.writeFileSync)(c,JSON.stringify({schemaVersion:1,project:o??null,buildOutDir:(0,node_path_1.join)(s,"dist"),timestamp:new Date().toISOString(),note:"Phase 2 baseline marker. Per-chunk JSON is materialised by the running lensmcp-mcp session (the self-contained bundle) once the supervisor wires the plugin bus."},null,2)+`
|
|
2
|
+
`),console.log(`[agent-build] baseline written: ${c}`)}catch(p){console.warn(`[agent-build] could not write baseline: ${p.message}`)}}return{success:l}}r(agentBuildExecutor,"agentBuildExecutor"),i(agentBuildExecutor,"agentBuildExecutor");function locateBin(n,e){for(const t of e){const o=(0,node_path_1.join)(t,"node_modules",".bin",n);if((0,node_fs_2.existsSync)(o))return o}}r(locateBin,"locateBin"),i(locateBin,"locateBin");function resolveFirstExisting(n,e){for(const t of n){const o=(0,node_path_1.join)(e,t);if((0,node_fs_2.existsSync)(o))return o}}r(resolveFirstExisting,"resolveFirstExisting"),i(resolveFirstExisting,"resolveFirstExisting");
|
|
@@ -1,425 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const node_child_process_1 =
|
|
5
|
-
const node_fs_1 = require("node:fs");
|
|
6
|
-
const node_path_1 = require("node:path");
|
|
7
|
-
const lens_frontend_1 = require("../../lens-frontend");
|
|
8
|
-
/**
|
|
9
|
-
* `agent-dev` — long-running executor that spawns:
|
|
10
|
-
*
|
|
11
|
-
* 1. The host project's Vite dev server (with `@lensmcp/vite-plugin`
|
|
12
|
-
* already wired by `setup-vite`).
|
|
13
|
-
* 2. (Optional) a Chrome sidecar pointed at the dev URL so CDP
|
|
14
|
-
* collectors see what the user sees.
|
|
15
|
-
* 3. The LensMCP MCP server, so an agent can connect.
|
|
16
|
-
*
|
|
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).
|
|
23
|
-
*
|
|
24
|
-
* Streams everyone's stdio to this terminal so the developer can see
|
|
25
|
-
* what's happening. Ctrl-C triggers a clean teardown.
|
|
26
|
-
*/
|
|
27
|
-
async function agentDevExecutor(options, context) {
|
|
28
|
-
const opts = {
|
|
29
|
-
kind: options.kind ?? 'vite-react',
|
|
30
|
-
chrome: options.chrome ?? true,
|
|
31
|
-
headless: options.headless ?? true,
|
|
32
|
-
node: options.node ?? true,
|
|
33
|
-
devTarget: options.devTarget,
|
|
34
|
-
projectRoot: options.projectRoot,
|
|
35
|
-
port: options.port,
|
|
36
|
-
openUrl: options.openUrl,
|
|
37
|
-
devCommand: options.devCommand,
|
|
38
|
-
wsPort: options.wsPort,
|
|
39
|
-
};
|
|
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}").`);
|
|
42
|
-
return { success: false };
|
|
43
|
-
}
|
|
44
|
-
const project = context.projectName;
|
|
45
|
-
const projectRoot = opts.projectRoot
|
|
46
|
-
? (0, node_path_1.resolve)(context.root, opts.projectRoot)
|
|
47
|
-
: project && context.projectsConfigurations
|
|
48
|
-
? (0, node_path_1.resolve)(context.root, context.projectsConfigurations.projects[project]?.root ?? '.')
|
|
49
|
-
: context.root;
|
|
50
|
-
// ---- NestJS branch ----
|
|
51
|
-
if (opts.kind === 'nestjs') {
|
|
52
|
-
return runNestjs(projectRoot, context);
|
|
53
|
-
}
|
|
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
|
-
});
|
|
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.
|
|
71
|
-
// 2. LensMCP MCP — prebuilt bundle from `apps/lensmcp-mcp/dist/main.js`.
|
|
72
|
-
// In a host workspace post-`lensmcp install`, this is the bundled
|
|
73
|
-
// CLI binary that ships with `lensmcp`. For dev in-repo we
|
|
74
|
-
// locate the workspace's own build artefact.
|
|
75
|
-
const mcpBundle = (0, lens_frontend_1.findMcpBundle)(context.root);
|
|
76
|
-
if (!mcpBundle) {
|
|
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`).");
|
|
78
|
-
}
|
|
79
|
-
const children = [];
|
|
80
|
-
const respawnCounts = new Map();
|
|
81
|
-
let exitRequested = false;
|
|
82
|
-
function spawnChild(label, bin, args, env, optional = false, respawn) {
|
|
83
|
-
const child = (0, node_child_process_1.spawn)(bin, args, {
|
|
84
|
-
cwd: projectRoot,
|
|
85
|
-
env: { ...process.env, ...(env ?? {}) },
|
|
86
|
-
stdio: 'inherit',
|
|
87
|
-
});
|
|
88
|
-
children.push(child);
|
|
89
|
-
child.on('exit', (code, sig) => {
|
|
90
|
-
if (exitRequested)
|
|
91
|
-
return;
|
|
92
|
-
if (optional) {
|
|
93
|
-
// Best-effort children (e.g. browser capture) may exit on their
|
|
94
|
-
// own (no Chrome, page closed) without tearing down the session.
|
|
95
|
-
// With a respawn policy they come back — a killed/crashed Chrome
|
|
96
|
-
// otherwise silently ends visual capture for the whole session.
|
|
97
|
-
const used = respawnCounts.get(label) ?? 0;
|
|
98
|
-
if (respawn && used < respawn.max) {
|
|
99
|
-
respawnCounts.set(label, used + 1);
|
|
100
|
-
console.warn(`[agent-dev] ${label} exited (code=${code} sig=${sig}); respawning in ${Math.round(respawn.delayMs / 1000)}s (${used + 1}/${respawn.max}).`);
|
|
101
|
-
const t = setTimeout(() => {
|
|
102
|
-
if (!exitRequested)
|
|
103
|
-
spawnChild(label, bin, args, env, optional, respawn);
|
|
104
|
-
}, respawn.delayMs);
|
|
105
|
-
t.unref?.();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
console.warn(`[agent-dev] ${label} exited (code=${code} sig=${sig}); continuing.`);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
console.warn(`[agent-dev] ${label} exited unexpectedly (code=${code} sig=${sig}); shutting down others.`);
|
|
112
|
-
shutdown();
|
|
113
|
-
});
|
|
114
|
-
return child;
|
|
115
|
-
}
|
|
116
|
-
function shutdown() {
|
|
117
|
-
if (exitRequested)
|
|
118
|
-
return;
|
|
119
|
-
exitRequested = true;
|
|
120
|
-
for (const c of children) {
|
|
121
|
-
try {
|
|
122
|
-
c.kill('SIGTERM');
|
|
123
|
-
}
|
|
124
|
-
catch { /* swallow */ }
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
process.on('SIGINT', shutdown);
|
|
128
|
-
process.on('SIGTERM', shutdown);
|
|
129
|
-
// Cross-process event bridge: the Vite plugin (in the dev-server child)
|
|
130
|
-
// and the MCP server (in its own child) rendezvous on a shared JSONL
|
|
131
|
-
// event file. The plugin appends browser events; the server tails it
|
|
132
|
-
// via `startEventIngest`. Truncate it on start so a fresh run doesn't
|
|
133
|
-
// replay a previous session's events.
|
|
134
|
-
const eventFile = process.env['LENSMCP_EVENT_FILE'] ?? (0, node_path_1.join)(context.root, '.lensmcp', 'events.jsonl');
|
|
135
|
-
try {
|
|
136
|
-
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(eventFile), { recursive: true });
|
|
137
|
-
// Truncate ONLY a stale file: another producer (e.g. the instrumented API in
|
|
138
|
-
// its own agent-dev) may be mid-session on the same bridge — wiping its
|
|
139
|
-
// events would blind every reader. "Active" = modified in the last 60s.
|
|
140
|
-
const st = (0, node_fs_1.statSync)(eventFile, { throwIfNoEntry: false });
|
|
141
|
-
if (!st || Date.now() - st.mtimeMs > 60_000) {
|
|
142
|
-
(0, node_fs_1.writeFileSync)(eventFile, '');
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
console.log('[agent-dev] event bridge is active (written <60s ago) — appending, not truncating.');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
/* non-fatal — ingest tolerates a missing file */
|
|
150
|
-
}
|
|
151
|
-
const bridgeEnv = { LENSMCP_EVENT_FILE: eventFile };
|
|
152
|
-
console.log(`[agent-dev] event bridge → ${eventFile}`);
|
|
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).
|
|
175
|
-
if (mcpBundle) {
|
|
176
|
-
console.log(`[agent-dev] starting LensMCP MCP server (${mcpBundle})`);
|
|
177
|
-
spawnChild('lensmcp-mcp', process.execPath, [mcpBundle], {
|
|
178
|
-
...bridgeEnv,
|
|
179
|
-
LENSMCP_TRANSPORT: process.env['LENSMCP_TRANSPORT'] ?? 'http',
|
|
180
|
-
LENSMCP_PORT: process.env['LENSMCP_PORT'] ?? '3000',
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
// Wait until any child exits.
|
|
184
|
-
return new Promise((res) => {
|
|
185
|
-
const tick = () => {
|
|
186
|
-
if (exitRequested) {
|
|
187
|
-
Promise.allSettled(children.map((c) => new Promise((r) => {
|
|
188
|
-
if (c.exitCode !== null)
|
|
189
|
-
r();
|
|
190
|
-
else
|
|
191
|
-
c.on('exit', () => r());
|
|
192
|
-
}))).then(() => res({ success: true }));
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
setTimeout(tick, 500);
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
tick();
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
// ---------- nestjs branch ----------
|
|
202
|
-
async function runNestjs(projectRoot, context) {
|
|
203
|
-
// The built entry lives in different places per setup:
|
|
204
|
-
// 1. the project build target's declared outputPath (most precise),
|
|
205
|
-
// 2. <projectRoot>/dist/main.js (standalone tsc layout),
|
|
206
|
-
// 3. <workspaceRoot>/dist/<projectRoot>/main.js (standard Nx layout).
|
|
207
|
-
const projectName = context.projectName;
|
|
208
|
-
const buildTarget = projectName
|
|
209
|
-
? context.projectsConfigurations?.projects[projectName]?.targets?.['build']
|
|
210
|
-
: undefined;
|
|
211
|
-
const outputPath = buildTarget?.options?.['outputPath'];
|
|
212
|
-
const candidates = [
|
|
213
|
-
...(typeof outputPath === 'string' ? [(0, node_path_1.join)(context.root, outputPath, 'main.js')] : []),
|
|
214
|
-
(0, node_path_1.join)(projectRoot, 'dist', 'main.js'),
|
|
215
|
-
(0, node_path_1.join)(context.root, 'dist', (0, node_path_1.relative)(context.root, projectRoot), 'main.js'),
|
|
216
|
-
];
|
|
217
|
-
const main = candidates.find((c) => (0, node_fs_1.existsSync)(c));
|
|
218
|
-
if (!main) {
|
|
219
|
-
console.error(`[agent-dev] No built NestJS entry found. Looked in:\n` +
|
|
220
|
-
candidates.map((c) => ` • ${c}`).join('\n') +
|
|
221
|
-
`\nBuild first: \`nx build ${projectName ?? '<project>'}\`.`);
|
|
222
|
-
return { success: false };
|
|
223
|
-
}
|
|
224
|
-
console.log(`[agent-dev] node ${main}`);
|
|
225
|
-
const child = (0, node_child_process_1.spawn)(process.execPath, [main], {
|
|
226
|
-
cwd: projectRoot,
|
|
227
|
-
stdio: 'inherit',
|
|
228
|
-
env: {
|
|
229
|
-
...process.env,
|
|
230
|
-
LENSMCP_TRANSPORT: process.env['LENSMCP_TRANSPORT'] ?? 'http',
|
|
231
|
-
LENSMCP_EVENT_FILE: process.env['LENSMCP_EVENT_FILE'] ??
|
|
232
|
-
(0, node_path_1.join)(context.root, '.lensmcp', 'events.jsonl'),
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
return new Promise((res) => {
|
|
236
|
-
const onSignal = () => {
|
|
237
|
-
try {
|
|
238
|
-
child.kill('SIGTERM');
|
|
239
|
-
}
|
|
240
|
-
catch { /* swallow */ }
|
|
241
|
-
};
|
|
242
|
-
process.on('SIGINT', onSignal);
|
|
243
|
-
process.on('SIGTERM', onSignal);
|
|
244
|
-
child.on('exit', (code) => {
|
|
245
|
-
process.off('SIGINT', onSignal);
|
|
246
|
-
process.off('SIGTERM', onSignal);
|
|
247
|
-
res({ success: code === 0 });
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
// ---------- imported / bring-your-own-build branch ----------
|
|
252
|
-
/**
|
|
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.
|
|
267
|
-
*/
|
|
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
|
-
}
|
|
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');
|
|
332
|
-
try {
|
|
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
|
-
}
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
/* non-fatal — ingest tolerates a missing file */
|
|
344
|
-
}
|
|
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
|
-
});
|
|
362
|
-
}
|
|
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
|
-
}
|
|
403
|
-
}
|
|
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
|
-
});
|
|
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
|
+
"use strict";var L=Object.defineProperty;var E=(t,o)=>L(t,"name",{value:o,configurable:!0});var T=Object.defineProperty,l=E((t,o)=>T(t,"name",{value:o,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=agentDevExecutor;const node_child_process_1=require("node:child_process"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),lens_frontend_1=require("../../lens-frontend");async function agentDevExecutor(t,o){const n={kind:t.kind??"vite-react",chrome:t.chrome??!0,headless:t.headless??!0,node:t.node??!0,devTarget:t.devTarget,projectRoot:t.projectRoot,port:t.port,openUrl:t.openUrl,devCommand:t.devCommand,wsPort:t.wsPort};if(n.kind!=="vite-react"&&n.kind!=="nestjs"&&n.kind!=="imported")return console.error(`[agent-dev] supported kinds are "vite-react", "nestjs", "imported" (got "${n.kind}").`),{success:!1};const P=o.projectName,d=n.projectRoot?(0,node_path_1.resolve)(o.root,n.projectRoot):P&&o.projectsConfigurations?(0,node_path_1.resolve)(o.root,o.projectsConfigurations.projects[P]?.root??"."):o.root;if(n.kind==="nestjs")return runNestjs(d,o);if(n.kind==="imported")return runImported(d,o,{devCommand:n.devCommand,chrome:n.chrome,headless:n.headless,openUrl:n.openUrl,wsPort:n.wsPort??5747,node:n.node});const a=(0,lens_frontend_1.findMcpBundle)(o.root);a||console.warn("[agent-dev] No lensmcp-mcp bundle found \u2014 skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/main.js`).");const S=[],c=new Map;let s=!1;function p(e,r,v,g,C=!1,i){const w=(0,node_child_process_1.spawn)(r,v,{cwd:d,env:{...process.env,...g??{}},stdio:"inherit"});return S.push(w),w.on("exit",(f,h)=>{if(!s){if(C){const N=c.get(e)??0;if(i&&N<i.max){c.set(e,N+1),console.warn(`[agent-dev] ${e} exited (code=${f} sig=${h}); respawning in ${Math.round(i.delayMs/1e3)}s (${N+1}/${i.max}).`),setTimeout(()=>{s||p(e,r,v,g,C,i)},i.delayMs).unref?.();return}console.warn(`[agent-dev] ${e} exited (code=${f} sig=${h}); continuing.`);return}console.warn(`[agent-dev] ${e} exited unexpectedly (code=${f} sig=${h}); shutting down others.`),u()}}),w}E(p,"d"),l(p,"spawnChild");function u(){if(!s){s=!0;for(const e of S)try{e.kill("SIGTERM")}catch{}}}E(u,"a"),l(u,"shutdown"),process.on("SIGINT",u),process.on("SIGTERM",u);const m=process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(o.root,".lensmcp","events.jsonl");try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(m),{recursive:!0});const e=(0,node_fs_1.statSync)(m,{throwIfNoEntry:!1});!e||Date.now()-e.mtimeMs>6e4?(0,node_fs_1.writeFileSync)(m,""):console.log("[agent-dev] event bridge is active (written <60s ago) \u2014 appending, not truncating.")}catch{}const M={LENSMCP_EVENT_FILE:m};console.log(`[agent-dev] event bridge \u2192 ${m}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:d,workspaceRoot:o.root,...P?{project:P}:{},eventFile:m,...n.port!==void 0?{port:n.port}:{},chrome:n.chrome,headless:n.headless,...n.openUrl?{openUrl:n.openUrl}:{},log:l(e=>console.log(e),"log")},p)}catch(e){return console.error(e.message),u(),{success:!1}}return a&&(console.log(`[agent-dev] starting LensMCP MCP server (${a})`),p("lensmcp-mcp",process.execPath,[a],{...M,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:process.env.LENSMCP_PORT??"3000"})),new Promise(e=>{const r=l(()=>{s?Promise.allSettled(S.map(v=>new Promise(g=>{v.exitCode!==null?g():v.on("exit",()=>g())}))).then(()=>e({success:!0})):setTimeout(r,500)},"tick");r()})}E(agentDevExecutor,"agentDevExecutor"),l(agentDevExecutor,"agentDevExecutor");async function runNestjs(t,o){const n=o.projectName,P=(n?o.projectsConfigurations?.projects[n]?.targets?.build:void 0)?.options?.outputPath,d=[...typeof P=="string"?[(0,node_path_1.join)(o.root,P,"main.js")]:[],(0,node_path_1.join)(t,"dist","main.js"),(0,node_path_1.join)(o.root,"dist",(0,node_path_1.relative)(o.root,t),"main.js")],a=d.find(c=>(0,node_fs_1.existsSync)(c));if(!a)return console.error(`[agent-dev] No built NestJS entry found. Looked in:
|
|
2
|
+
`+d.map(c=>` \u2022 ${c}`).join(`
|
|
3
|
+
`)+`
|
|
4
|
+
Build first: \`nx build ${n??"<project>"}\`.`),{success:!1};console.log(`[agent-dev] node ${a}`);const S=(0,node_child_process_1.spawn)(process.execPath,[a],{cwd:t,stdio:"inherit",env:{...process.env,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_EVENT_FILE:process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(o.root,".lensmcp","events.jsonl")}});return new Promise(c=>{const s=l(()=>{try{S.kill("SIGTERM")}catch{}},"onSignal");process.on("SIGINT",s),process.on("SIGTERM",s),S.on("exit",p=>{process.off("SIGINT",s),process.off("SIGTERM",s),c({success:p===0})})})}E(runNestjs,"runNestjs"),l(runNestjs,"runNestjs");async function runImported(t,o,n){if(!n.devCommand)return console.error('[agent-dev] kind="imported" needs a `devCommand` (your dev-server command, e.g. "npm run dev").\n Add it to the agent-dev target options, or pass `--devCommand "npm run dev"`.'),{success:!1};const P=(0,lens_frontend_1.findBridgeBundle)(o.root);if(!P)return console.error("[agent-dev] No lensmcp bridge bundle found. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/bridge.js`)."),{success:!1};const d=(0,lens_frontend_1.findMcpBundle)(o.root);d||console.warn("[agent-dev] No lensmcp-mcp bundle found \u2014 skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp`.");const a=[],S=new Map;let c=!1;function s(e,r,v,g,C=!1,i,w=!1){const f=(0,node_child_process_1.spawn)(r,v,{cwd:t,env:{...process.env,...g??{}},stdio:"inherit",shell:w});return a.push(f),f.on("exit",(h,N)=>{if(!c){if(C){const _=S.get(e)??0;if(i&&_<i.max){S.set(e,_+1),console.warn(`[agent-dev] ${e} exited (code=${h} sig=${N}); respawning in ${Math.round(i.delayMs/1e3)}s (${_+1}/${i.max}).`),setTimeout(()=>{c||s(e,r,v,g,C,i,w)},i.delayMs).unref?.();return}console.warn(`[agent-dev] ${e} exited (code=${h} sig=${N}); continuing.`);return}console.warn(`[agent-dev] ${e} exited unexpectedly (code=${h} sig=${N}); shutting down others.`),p()}}),f}E(s,"r"),l(s,"spawnChild");function p(){if(!c){c=!0;for(const e of a)try{e.kill("SIGTERM")}catch{}}}E(p,"d"),l(p,"shutdown"),process.on("SIGINT",p),process.on("SIGTERM",p);const u=process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(o.root,".lensmcp","events.jsonl");try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(u),{recursive:!0});const e=(0,node_fs_1.statSync)(u,{throwIfNoEntry:!1});!e||Date.now()-e.mtimeMs>6e4?(0,node_fs_1.writeFileSync)(u,""):console.log("[agent-dev] event bridge is active (written <60s ago) \u2014 appending, not truncating.")}catch{}const m={LENSMCP_EVENT_FILE:u};console.log(`[agent-dev] event bridge \u2192 ${u}`),console.log(`[agent-dev] starting bridge \u2192 ws://127.0.0.1:${n.wsPort}`),s("bridge",process.execPath,[P],{...m,LENSMCP_WS_HOST:process.env.LENSMCP_WS_HOST??"127.0.0.1",LENSMCP_WS_PORT:String(n.wsPort)}),d&&(console.log(`[agent-dev] starting LensMCP MCP server (${d})`),s("lensmcp-mcp",process.execPath,[d],{...m,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:process.env.LENSMCP_PORT??"3000"}));const M={...m,LENSMCP_WS_PORT:String(n.wsPort)};if(n.node)if([t,o.root].some(e=>(0,node_fs_1.existsSync)((0,node_path_1.join)(e,"node_modules","@lensmcp","node-instrumentation")))){const e="--import @lensmcp/node-instrumentation/register";M.NODE_OPTIONS=process.env.NODE_OPTIONS?`${process.env.NODE_OPTIONS} ${e}`:e,console.log("[agent-dev] backend taps: NODE_OPTIONS += --import @lensmcp/node-instrumentation/register")}else console.warn("[agent-dev] @lensmcp/node-instrumentation not installed \u2014 skipping backend taps. Add it: `npm i -D @lensmcp/node-instrumentation`.");if(console.log(`[agent-dev] front-end seam: add \`import '@lensmcp/client-runtime/auto'\` to your app entry (connects to ws://127.0.0.1:${n.wsPort}).`),console.log(`[agent-dev] starting dev command: ${n.devCommand}`),s("dev",n.devCommand,[],M,!1,void 0,!0),n.chrome){const e=(0,lens_frontend_1.findCaptureRunner)(o.root),r=n.openUrl;e?r?(console.log(`[agent-dev] starting browser capture \u2192 ${r}`),s("browser-capture",process.execPath,[e],{...m,LENSMCP_DEV_URL:r,LENSMCP_TOKENS_FILE:(0,node_path_1.join)(o.root,"lensmcp.tokens.json"),LENSMCP_RULES_FILE:(0,node_path_1.join)(o.root,"lensmcp.rules.json"),LENSMCP_HEADLESS:n.headless===!1?"false":"true"},!0,{delayMs:1e4,max:5})):console.warn("[agent-dev] no `openUrl` provided \u2014 skipping Chrome capture (set openUrl to your dev URL)."):console.warn("[agent-dev] browser-capture runner not found \u2014 skipping live capture. Install the self-contained CLI: `npm i -D lensmcp`.")}return new Promise(e=>{const r=l(()=>{c?Promise.allSettled(a.map(v=>new Promise(g=>{v.exitCode!==null?g():v.on("exit",()=>g())}))).then(()=>e({success:!0})):setTimeout(r,500)},"tick");r()})}E(runImported,"runImported"),l(runImported,"runImported");
|