@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.
Files changed (38) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +141 -0
  3. package/executors/agent-build/agent-build.d.ts +21 -0
  4. package/executors/agent-build/agent-build.d.ts.map +1 -0
  5. package/executors/agent-build/agent-build.js +86 -0
  6. package/executors/agent-build/schema.d.ts +5 -0
  7. package/executors/agent-build/schema.json +25 -0
  8. package/executors/agent-dev/agent-dev.d.ts +24 -0
  9. package/executors/agent-dev/agent-dev.d.ts.map +1 -0
  10. package/executors/agent-dev/agent-dev.js +327 -0
  11. package/executors/agent-dev/schema.d.ts +9 -0
  12. package/executors/agent-dev/schema.json +41 -0
  13. package/executors/agent-verify/agent-verify.d.ts +22 -0
  14. package/executors/agent-verify/agent-verify.d.ts.map +1 -0
  15. package/executors/agent-verify/agent-verify.js +156 -0
  16. package/executors/agent-verify/schema.d.ts +7 -0
  17. package/executors/agent-verify/schema.json +23 -0
  18. package/executors.json +22 -0
  19. package/generators/init/init.d.ts +5 -0
  20. package/generators/init/init.d.ts.map +1 -0
  21. package/generators/init/init.js +167 -0
  22. package/generators/init/schema.d.ts +4 -0
  23. package/generators/init/schema.json +22 -0
  24. package/generators/setup-nest/schema.d.ts +4 -0
  25. package/generators/setup-nest/schema.json +18 -0
  26. package/generators/setup-nest/setup-nest.d.ts +41 -0
  27. package/generators/setup-nest/setup-nest.d.ts.map +1 -0
  28. package/generators/setup-nest/setup-nest.js +281 -0
  29. package/generators/setup-vite/schema.d.ts +4 -0
  30. package/generators/setup-vite/schema.json +18 -0
  31. package/generators/setup-vite/setup-vite.d.ts +31 -0
  32. package/generators/setup-vite/setup-vite.d.ts.map +1 -0
  33. package/generators/setup-vite/setup-vite.js +125 -0
  34. package/generators.json +22 -0
  35. package/index.d.ts +14 -0
  36. package/index.d.ts.map +1 -0
  37. package/index.js +21 -0
  38. 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,9 @@
1
+ export interface AgentDevExecutorSchema {
2
+ kind?: 'vite-react' | 'nestjs' | 'nextjs';
3
+ devTarget?: string;
4
+ projectRoot?: string;
5
+ chrome?: boolean;
6
+ headless?: boolean;
7
+ port?: number;
8
+ openUrl?: string;
9
+ }
@@ -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,7 @@
1
+ export interface AgentVerifyExecutorSchema {
2
+ kind?: 'vite-react';
3
+ projectRoot?: string;
4
+ skipTypecheck?: boolean;
5
+ skipLint?: boolean;
6
+ skipBuild?: boolean;
7
+ }
@@ -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,5 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ import type { InitGeneratorSchema } from './schema';
3
+ export declare function initGenerator(tree: Tree, rawOptions?: InitGeneratorSchema): Promise<void>;
4
+ export default initGenerator;
5
+ //# sourceMappingURL=init.d.ts.map
@@ -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"}