@lensmcp/nx-plugin 1.0.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/LICENSE +202 -0
- package/README.md +141 -0
- package/executors/agent-build/agent-build.d.ts +21 -0
- package/executors/agent-build/agent-build.d.ts.map +1 -0
- package/executors/agent-build/agent-build.js +86 -0
- package/executors/agent-build/schema.d.ts +5 -0
- package/executors/agent-build/schema.json +25 -0
- package/executors/agent-dev/agent-dev.d.ts +24 -0
- package/executors/agent-dev/agent-dev.d.ts.map +1 -0
- package/executors/agent-dev/agent-dev.js +327 -0
- package/executors/agent-dev/schema.d.ts +9 -0
- package/executors/agent-dev/schema.json +41 -0
- package/executors/agent-verify/agent-verify.d.ts +22 -0
- package/executors/agent-verify/agent-verify.d.ts.map +1 -0
- package/executors/agent-verify/agent-verify.js +156 -0
- package/executors/agent-verify/schema.d.ts +7 -0
- package/executors/agent-verify/schema.json +23 -0
- package/executors.json +22 -0
- package/generators/init/init.d.ts +5 -0
- package/generators/init/init.d.ts.map +1 -0
- package/generators/init/init.js +167 -0
- package/generators/init/schema.d.ts +4 -0
- package/generators/init/schema.json +22 -0
- package/generators/setup-nest/schema.d.ts +4 -0
- package/generators/setup-nest/schema.json +18 -0
- package/generators/setup-nest/setup-nest.d.ts +41 -0
- package/generators/setup-nest/setup-nest.d.ts.map +1 -0
- package/generators/setup-nest/setup-nest.js +281 -0
- package/generators/setup-vite/schema.d.ts +4 -0
- package/generators/setup-vite/schema.json +18 -0
- package/generators/setup-vite/setup-vite.d.ts +31 -0
- package/generators/setup-vite/setup-vite.d.ts.map +1 -0
- package/generators/setup-vite/setup-vite.js +125 -0
- package/generators.json +22 -0
- package/index.d.ts +14 -0
- package/index.d.ts.map +1 -0
- package/index.js +21 -0
- package/package.json +47 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = agentDevExecutor;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
/**
|
|
8
|
+
* `agent-dev` — long-running executor that spawns:
|
|
9
|
+
*
|
|
10
|
+
* 1. The host project's Vite dev server (with `@lensmcp/vite-plugin`
|
|
11
|
+
* already wired by `setup-vite`).
|
|
12
|
+
* 2. (Optional) a Chrome sidecar pointed at the dev URL so CDP
|
|
13
|
+
* collectors see what the user sees.
|
|
14
|
+
* 3. The LensMCP MCP server, so an agent can connect.
|
|
15
|
+
*
|
|
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.
|
|
19
|
+
*
|
|
20
|
+
* Streams everyone's stdio to this terminal so the developer can see
|
|
21
|
+
* what's happening. Ctrl-C triggers a clean teardown.
|
|
22
|
+
*/
|
|
23
|
+
async function agentDevExecutor(options, context) {
|
|
24
|
+
const opts = {
|
|
25
|
+
kind: options.kind ?? 'vite-react',
|
|
26
|
+
chrome: options.chrome ?? true,
|
|
27
|
+
headless: options.headless ?? true,
|
|
28
|
+
devTarget: options.devTarget,
|
|
29
|
+
projectRoot: options.projectRoot,
|
|
30
|
+
port: options.port,
|
|
31
|
+
openUrl: options.openUrl,
|
|
32
|
+
};
|
|
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}").`);
|
|
35
|
+
return { success: false };
|
|
36
|
+
}
|
|
37
|
+
const project = context.projectName;
|
|
38
|
+
const projectRoot = opts.projectRoot
|
|
39
|
+
? (0, node_path_1.resolve)(context.root, opts.projectRoot)
|
|
40
|
+
: project && context.projectsConfigurations
|
|
41
|
+
? (0, node_path_1.resolve)(context.root, context.projectsConfigurations.projects[project]?.root ?? '.')
|
|
42
|
+
: context.root;
|
|
43
|
+
// ---- NestJS branch ----
|
|
44
|
+
if (opts.kind === 'nestjs') {
|
|
45
|
+
return runNestjs(projectRoot, context);
|
|
46
|
+
}
|
|
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');
|
|
64
|
+
}
|
|
65
|
+
// 2. LensMCP MCP — prebuilt bundle from `apps/lensmcp-mcp/dist/main.js`.
|
|
66
|
+
// In a host workspace post-`lensmcp install`, this is the bundled
|
|
67
|
+
// CLI binary that ships with `lensmcp`. For dev in-repo we
|
|
68
|
+
// locate the workspace's own build artefact.
|
|
69
|
+
const mcpBundle = findMcpBundle(context.root);
|
|
70
|
+
if (!mcpBundle) {
|
|
71
|
+
console.warn('[agent-dev] No lensmcp-mcp bundle found — skipping MCP server.');
|
|
72
|
+
}
|
|
73
|
+
const children = [];
|
|
74
|
+
const respawnCounts = new Map();
|
|
75
|
+
let exitRequested = false;
|
|
76
|
+
function spawnChild(label, bin, args, env, optional = false, respawn) {
|
|
77
|
+
const child = (0, node_child_process_1.spawn)(bin, args, {
|
|
78
|
+
cwd: projectRoot,
|
|
79
|
+
env: { ...process.env, ...(env ?? {}) },
|
|
80
|
+
stdio: 'inherit',
|
|
81
|
+
});
|
|
82
|
+
children.push(child);
|
|
83
|
+
child.on('exit', (code, sig) => {
|
|
84
|
+
if (exitRequested)
|
|
85
|
+
return;
|
|
86
|
+
if (optional) {
|
|
87
|
+
// Best-effort children (e.g. browser capture) may exit on their
|
|
88
|
+
// own (no Chrome, page closed) without tearing down the session.
|
|
89
|
+
// With a respawn policy they come back — a killed/crashed Chrome
|
|
90
|
+
// otherwise silently ends visual capture for the whole session.
|
|
91
|
+
const used = respawnCounts.get(label) ?? 0;
|
|
92
|
+
if (respawn && used < respawn.max) {
|
|
93
|
+
respawnCounts.set(label, used + 1);
|
|
94
|
+
console.warn(`[agent-dev] ${label} exited (code=${code} sig=${sig}); respawning in ${Math.round(respawn.delayMs / 1000)}s (${used + 1}/${respawn.max}).`);
|
|
95
|
+
const t = setTimeout(() => {
|
|
96
|
+
if (!exitRequested)
|
|
97
|
+
spawnChild(label, bin, args, env, optional, respawn);
|
|
98
|
+
}, respawn.delayMs);
|
|
99
|
+
t.unref?.();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.warn(`[agent-dev] ${label} exited (code=${code} sig=${sig}); continuing.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.warn(`[agent-dev] ${label} exited unexpectedly (code=${code} sig=${sig}); shutting down others.`);
|
|
106
|
+
shutdown();
|
|
107
|
+
});
|
|
108
|
+
return child;
|
|
109
|
+
}
|
|
110
|
+
function shutdown() {
|
|
111
|
+
if (exitRequested)
|
|
112
|
+
return;
|
|
113
|
+
exitRequested = true;
|
|
114
|
+
for (const c of children) {
|
|
115
|
+
try {
|
|
116
|
+
c.kill('SIGTERM');
|
|
117
|
+
}
|
|
118
|
+
catch { /* swallow */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
process.on('SIGINT', shutdown);
|
|
122
|
+
process.on('SIGTERM', shutdown);
|
|
123
|
+
// Cross-process event bridge: the Vite plugin (in the dev-server child)
|
|
124
|
+
// and the MCP server (in its own child) rendezvous on a shared JSONL
|
|
125
|
+
// event file. The plugin appends browser events; the server tails it
|
|
126
|
+
// via `startEventIngest`. Truncate it on start so a fresh run doesn't
|
|
127
|
+
// replay a previous session's events.
|
|
128
|
+
const eventFile = process.env['LENSMCP_EVENT_FILE'] ?? (0, node_path_1.join)(context.root, '.lensmcp', 'events.jsonl');
|
|
129
|
+
try {
|
|
130
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(eventFile), { recursive: true });
|
|
131
|
+
// Truncate ONLY a stale file: another producer (e.g. the instrumented API in
|
|
132
|
+
// its own agent-dev) may be mid-session on the same bridge — wiping its
|
|
133
|
+
// events would blind every reader. "Active" = modified in the last 60s.
|
|
134
|
+
const st = (0, node_fs_1.statSync)(eventFile, { throwIfNoEntry: false });
|
|
135
|
+
if (!st || Date.now() - st.mtimeMs > 60_000) {
|
|
136
|
+
(0, node_fs_1.writeFileSync)(eventFile, '');
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log('[agent-dev] event bridge is active (written <60s ago) — appending, not truncating.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* non-fatal — ingest tolerates a missing file */
|
|
144
|
+
}
|
|
145
|
+
const bridgeEnv = { LENSMCP_EVENT_FILE: eventFile };
|
|
146
|
+
console.log(`[agent-dev] event bridge → ${eventFile}`);
|
|
147
|
+
console.log(`[agent-dev] starting Vite for ${project ?? projectRoot}`);
|
|
148
|
+
spawnChild('vite', viteBin, viteArgs, bridgeEnv);
|
|
149
|
+
if (mcpBundle) {
|
|
150
|
+
console.log(`[agent-dev] starting LensMCP MCP server (${mcpBundle})`);
|
|
151
|
+
spawnChild('lensmcp-mcp', process.execPath, [mcpBundle], {
|
|
152
|
+
...bridgeEnv,
|
|
153
|
+
LENSMCP_TRANSPORT: process.env['LENSMCP_TRANSPORT'] ?? 'http',
|
|
154
|
+
LENSMCP_PORT: process.env['LENSMCP_PORT'] ?? '3000',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
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
|
+
// 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
|
+
// ---------- 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
|
+
}
|
|
268
|
+
/**
|
|
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.
|
|
273
|
+
*/
|
|
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;
|
|
282
|
+
}
|
|
283
|
+
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;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
cachedGlobalRoot = null;
|
|
289
|
+
}
|
|
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;
|
|
307
|
+
}
|
|
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;
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "LensmcpAgentDev",
|
|
4
|
+
"title": "agent-dev",
|
|
5
|
+
"description": "Run a host project under the LensMCP lens.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"kind": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["vite-react", "nestjs", "nextjs"],
|
|
12
|
+
"default": "vite-react"
|
|
13
|
+
},
|
|
14
|
+
"devTarget": {
|
|
15
|
+
"type": "string",
|
|
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
|
+
},
|
|
18
|
+
"projectRoot": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Override the project root passed to Vite. Defaults to the running project's root."
|
|
21
|
+
},
|
|
22
|
+
"chrome": {
|
|
23
|
+
"type": "boolean",
|
|
24
|
+
"default": true,
|
|
25
|
+
"description": "Launch a Chrome sidecar attached to the dev URL."
|
|
26
|
+
},
|
|
27
|
+
"headless": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"default": true
|
|
30
|
+
},
|
|
31
|
+
"port": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Override the Vite dev port (default: project's vite.config.ts setting or 5173)."
|
|
34
|
+
},
|
|
35
|
+
"openUrl": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Override the URL Chrome navigates to."
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": []
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ExecutorContext } from '@nx/devkit';
|
|
2
|
+
import type { AgentVerifyExecutorSchema } from './schema';
|
|
3
|
+
interface ExecutorResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `agent-verify` — the deterministic verification loop the agent runs
|
|
8
|
+
* to know "did my fix work?".
|
|
9
|
+
*
|
|
10
|
+
* Phase 2 covers the frontend slice: typecheck (`tsc --noEmit`), lint
|
|
11
|
+
* (`eslint`), and build (`vite build`). Each stage runs sequentially;
|
|
12
|
+
* a stage failure marks the verify as `failed` but later stages still
|
|
13
|
+
* run so the agent gets a complete picture, not just the first error.
|
|
14
|
+
*
|
|
15
|
+
* The report is written to `.lensmcp/verifications/<project>-<ts>.json`
|
|
16
|
+
* and to `.lensmcp/verifications/latest.json` for easy resource access.
|
|
17
|
+
* The MCP-side `agent://latest-verification` resource (Phase 2.5) reads
|
|
18
|
+
* from `latest.json`.
|
|
19
|
+
*/
|
|
20
|
+
export default function agentVerifyExecutor(options: AgentVerifyExecutorSchema, context: ExecutorContext): Promise<ExecutorResult>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=agent-verify.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = agentVerifyExecutor;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
/**
|
|
8
|
+
* `agent-verify` — the deterministic verification loop the agent runs
|
|
9
|
+
* to know "did my fix work?".
|
|
10
|
+
*
|
|
11
|
+
* Phase 2 covers the frontend slice: typecheck (`tsc --noEmit`), lint
|
|
12
|
+
* (`eslint`), and build (`vite build`). Each stage runs sequentially;
|
|
13
|
+
* a stage failure marks the verify as `failed` but later stages still
|
|
14
|
+
* run so the agent gets a complete picture, not just the first error.
|
|
15
|
+
*
|
|
16
|
+
* The report is written to `.lensmcp/verifications/<project>-<ts>.json`
|
|
17
|
+
* and to `.lensmcp/verifications/latest.json` for easy resource access.
|
|
18
|
+
* The MCP-side `agent://latest-verification` resource (Phase 2.5) reads
|
|
19
|
+
* from `latest.json`.
|
|
20
|
+
*/
|
|
21
|
+
async function agentVerifyExecutor(options, context) {
|
|
22
|
+
const opts = {
|
|
23
|
+
kind: options.kind ?? 'vite-react',
|
|
24
|
+
skipTypecheck: options.skipTypecheck ?? false,
|
|
25
|
+
skipLint: options.skipLint ?? false,
|
|
26
|
+
skipBuild: options.skipBuild ?? false,
|
|
27
|
+
projectRoot: options.projectRoot,
|
|
28
|
+
};
|
|
29
|
+
const projectName = context.projectName ?? null;
|
|
30
|
+
const projectRoot = opts.projectRoot
|
|
31
|
+
? (0, node_path_1.resolve)(context.root, opts.projectRoot)
|
|
32
|
+
: projectName && context.projectsConfigurations
|
|
33
|
+
? (0, node_path_1.resolve)(context.root, context.projectsConfigurations.projects[projectName]?.root ?? '.')
|
|
34
|
+
: context.root;
|
|
35
|
+
const startedAt = Date.now();
|
|
36
|
+
const stages = [];
|
|
37
|
+
// 1. typecheck — `tsc --noEmit -p <root>` if a tsconfig is present.
|
|
38
|
+
if (!opts.skipTypecheck) {
|
|
39
|
+
const tscBin = locateBin('tsc', [projectRoot, context.root]);
|
|
40
|
+
const tsconfig = resolveFirstExisting(['tsconfig.json'], projectRoot);
|
|
41
|
+
if (!tscBin || !tsconfig) {
|
|
42
|
+
stages.push({ name: 'typecheck', result: 'skipped', durationMs: 0, summary: 'no tsc/tsconfig' });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
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
|
+
});
|
|
51
|
+
stages.push({
|
|
52
|
+
name: 'typecheck',
|
|
53
|
+
result: r.status === 0 ? 'passed' : 'failed',
|
|
54
|
+
durationMs: Date.now() - t0,
|
|
55
|
+
exitCode: r.status ?? -1,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
stages.push({ name: 'typecheck', result: 'skipped', durationMs: 0 });
|
|
61
|
+
}
|
|
62
|
+
// 2. lint — `eslint .` if eslint is present.
|
|
63
|
+
if (!opts.skipLint) {
|
|
64
|
+
const eslintBin = locateBin('eslint', [projectRoot, context.root]);
|
|
65
|
+
if (!eslintBin) {
|
|
66
|
+
stages.push({ name: 'lint', result: 'skipped', durationMs: 0, summary: 'no eslint' });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const t0 = Date.now();
|
|
70
|
+
console.log('[agent-verify] eslint .');
|
|
71
|
+
const r = (0, node_child_process_1.spawnSync)(eslintBin, ['.'], { cwd: projectRoot, stdio: 'inherit' });
|
|
72
|
+
stages.push({
|
|
73
|
+
name: 'lint',
|
|
74
|
+
result: r.status === 0 ? 'passed' : 'failed',
|
|
75
|
+
durationMs: Date.now() - t0,
|
|
76
|
+
exitCode: r.status ?? -1,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
stages.push({ name: 'lint', result: 'skipped', durationMs: 0 });
|
|
82
|
+
}
|
|
83
|
+
// 3. build — `vite build` if vite is present.
|
|
84
|
+
if (!opts.skipBuild) {
|
|
85
|
+
const viteBin = locateBin('vite', [projectRoot, context.root]);
|
|
86
|
+
if (!viteBin) {
|
|
87
|
+
stages.push({ name: 'build', result: 'skipped', durationMs: 0, summary: 'no vite' });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const viteConfig = resolveFirstExisting(['vite.config.ts', 'vite.config.js', 'vite.config.mjs'], projectRoot) ??
|
|
91
|
+
(0, node_path_1.join)(projectRoot, 'vite.config.ts');
|
|
92
|
+
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
|
+
});
|
|
98
|
+
stages.push({
|
|
99
|
+
name: 'build',
|
|
100
|
+
result: r.status === 0 ? 'passed' : 'failed',
|
|
101
|
+
durationMs: Date.now() - t0,
|
|
102
|
+
exitCode: r.status ?? -1,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
stages.push({ name: 'build', result: 'skipped', durationMs: 0 });
|
|
108
|
+
}
|
|
109
|
+
const status = stages.some((s) => s.result === 'failed') ? 'failed' : 'passed';
|
|
110
|
+
const report = {
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
project: projectName,
|
|
113
|
+
timestamp: new Date(startedAt).toISOString(),
|
|
114
|
+
durationMs: Date.now() - startedAt,
|
|
115
|
+
status,
|
|
116
|
+
stages,
|
|
117
|
+
};
|
|
118
|
+
const outDir = (0, node_path_1.join)(context.root, '.lensmcp', 'verifications');
|
|
119
|
+
try {
|
|
120
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
121
|
+
const ts = startedAt;
|
|
122
|
+
// Project names like `@lensmcp/example-web` contain `/`. Flatten so
|
|
123
|
+
// the report filename is a single path segment.
|
|
124
|
+
const safeName = (projectName ?? 'app').replace(/[^a-zA-Z0-9_-]+/g, '-');
|
|
125
|
+
const filename = `${safeName}-${ts}.json`;
|
|
126
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, filename), JSON.stringify(report, null, 2) + '\n');
|
|
127
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'latest.json'), JSON.stringify(report, null, 2) + '\n');
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.warn(`[agent-verify] could not write report: ${e.message}`);
|
|
131
|
+
}
|
|
132
|
+
for (const s of report.stages) {
|
|
133
|
+
console.log(`[agent-verify] ${s.name}: ${s.result}${s.exitCode !== undefined ? ` (exit ${s.exitCode})` : ''}`);
|
|
134
|
+
}
|
|
135
|
+
console.log(`[agent-verify] ${status} in ${report.durationMs}ms`);
|
|
136
|
+
return { success: status === 'passed' };
|
|
137
|
+
}
|
|
138
|
+
function locateBin(name, roots) {
|
|
139
|
+
for (const root of roots) {
|
|
140
|
+
const candidate = (0, node_path_1.join)(root, 'node_modules', '.bin', name);
|
|
141
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
142
|
+
return candidate;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
function resolveFirstExisting(names, root) {
|
|
147
|
+
for (const n of names) {
|
|
148
|
+
const p = (0, node_path_1.join)(root, n);
|
|
149
|
+
if ((0, node_fs_1.existsSync)(p))
|
|
150
|
+
return p;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
// Unused helpers, kept available for future re-use by tools that may
|
|
155
|
+
// drive the report differently.
|
|
156
|
+
void node_path_1.dirname;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "LensmcpAgentVerify",
|
|
4
|
+
"title": "agent-verify",
|
|
5
|
+
"description": "Deterministic verification loop: typecheck → lint → build. Phase 2 frontend-only.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"kind": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["vite-react"],
|
|
12
|
+
"default": "vite-react"
|
|
13
|
+
},
|
|
14
|
+
"projectRoot": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Override the project root. Defaults to the running project's root."
|
|
17
|
+
},
|
|
18
|
+
"skipTypecheck": { "type": "boolean", "default": false },
|
|
19
|
+
"skipLint": { "type": "boolean", "default": false },
|
|
20
|
+
"skipBuild": { "type": "boolean", "default": false }
|
|
21
|
+
},
|
|
22
|
+
"required": []
|
|
23
|
+
}
|
package/executors.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"name": "@lensmcp/nx-plugin",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"executors": {
|
|
6
|
+
"agent-dev": {
|
|
7
|
+
"implementation": "./dist/executors/agent-dev/agent-dev",
|
|
8
|
+
"schema": "./dist/executors/agent-dev/schema.json",
|
|
9
|
+
"description": "Start a project with the LensMCP lens attached (Vite dev server + Chrome sidecar + MCP server)."
|
|
10
|
+
},
|
|
11
|
+
"agent-build": {
|
|
12
|
+
"implementation": "./dist/executors/agent-build/agent-build",
|
|
13
|
+
"schema": "./dist/executors/agent-build/schema.json",
|
|
14
|
+
"description": "Instrumented production build that produces a bundle report and updates the baseline."
|
|
15
|
+
},
|
|
16
|
+
"agent-verify": {
|
|
17
|
+
"implementation": "./dist/executors/agent-verify/agent-verify",
|
|
18
|
+
"schema": "./dist/executors/agent-verify/schema.json",
|
|
19
|
+
"description": "Deterministic verification: typecheck → lint → build, with a per-run report under .lensmcp/verifications/."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/generators/init/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,IAAI,EAEV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAgDpD,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,UAAU,GAAE,mBAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CAiFf;AAkCD,eAAe,aAAa,CAAC"}
|