@_deep4wee/agent-lens 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +141 -31
  2. package/dist/cli.d.mts +2 -1
  3. package/dist/cli.d.ts +2 -1
  4. package/dist/cli.js +1169 -957
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cli.mjs +1268 -974
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/dsl-BIjVN1M0.d.mts +204 -0
  9. package/dist/dsl-BIjVN1M0.d.ts +204 -0
  10. package/dist/index.d.mts +131 -155
  11. package/dist/index.d.ts +131 -155
  12. package/dist/index.js +1506 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +1479 -3
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/plugins/a11y-tree/index.d.mts +11 -0
  17. package/dist/plugins/a11y-tree/index.d.ts +11 -0
  18. package/dist/plugins/a11y-tree/index.js +190 -0
  19. package/dist/plugins/a11y-tree/index.js.map +1 -0
  20. package/dist/plugins/a11y-tree/index.mjs +155 -0
  21. package/dist/plugins/a11y-tree/index.mjs.map +1 -0
  22. package/dist/plugins/desktop-webview2/index.d.mts +14 -0
  23. package/dist/plugins/desktop-webview2/index.d.ts +14 -0
  24. package/dist/plugins/desktop-webview2/index.js +258 -0
  25. package/dist/plugins/desktop-webview2/index.js.map +1 -0
  26. package/dist/plugins/desktop-webview2/index.mjs +221 -0
  27. package/dist/plugins/desktop-webview2/index.mjs.map +1 -0
  28. package/dist/plugins/live-controller/index.d.mts +35 -0
  29. package/dist/plugins/live-controller/index.d.ts +35 -0
  30. package/dist/plugins/live-controller/index.js +303 -0
  31. package/dist/plugins/live-controller/index.js.map +1 -0
  32. package/dist/plugins/live-controller/index.mjs +261 -0
  33. package/dist/plugins/live-controller/index.mjs.map +1 -0
  34. package/dist/plugins/mock-ipc/index.d.mts +30 -0
  35. package/dist/plugins/mock-ipc/index.d.ts +30 -0
  36. package/dist/plugins/mock-ipc/index.js +210 -0
  37. package/dist/plugins/mock-ipc/index.js.map +1 -0
  38. package/dist/plugins/mock-ipc/index.mjs +181 -0
  39. package/dist/plugins/mock-ipc/index.mjs.map +1 -0
  40. package/dist/plugins/visual-diff/index.d.mts +30 -0
  41. package/dist/plugins/visual-diff/index.d.ts +30 -0
  42. package/dist/plugins/visual-diff/index.js +163 -0
  43. package/dist/plugins/visual-diff/index.js.map +1 -0
  44. package/dist/plugins/visual-diff/index.mjs +127 -0
  45. package/dist/plugins/visual-diff/index.mjs.map +1 -0
  46. package/docs/plugins.md +415 -0
  47. package/package.json +40 -2
  48. package/skills/agent-lens/SKILL.md +142 -37
  49. package/skills/agent-lens/examples/06-state-testing-with-mock-ipc.md +13 -12
  50. package/skills/agent-lens/examples/07-live-controller-interactive-loop.md +108 -0
  51. package/skills/agent-lens/examples/08-accessibility-semantic-inspection.md +80 -0
  52. package/skills/agent-lens/examples/09-visual-regression-and-pixel-diffing.md +66 -0
  53. package/skills/agent-lens/examples/10-authoring-custom-agent-plugins.md +85 -0
  54. package/skills/agent-lens/references/plugin-development.md +165 -0
package/dist/cli.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
  var __create = Object.create;
4
3
  var __defProp = Object.defineProperty;
@@ -24,18 +23,102 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
23
  ));
25
24
 
26
25
  // src/app/cli.ts
27
- var import_path8 = __toESM(require("path"));
28
- var import_fs8 = __toESM(require("fs"));
26
+ var import_path13 = __toESM(require("path"));
27
+ var import_fs12 = __toESM(require("fs"));
28
+ var import_child_process2 = require("child_process");
29
+ var import_jiti2 = require("jiti");
30
+
31
+ // src/shared/lib/config.ts
32
+ var import_path = __toESM(require("path"));
33
+ var import_fs = __toESM(require("fs"));
34
+ function loadConfig(cwd = process.cwd()) {
35
+ const jsonConfigPath = import_path.default.join(cwd, "agent-lens.json");
36
+ if (import_fs.default.existsSync(jsonConfigPath)) {
37
+ try {
38
+ const raw = import_fs.default.readFileSync(jsonConfigPath, "utf-8");
39
+ return JSON.parse(raw);
40
+ } catch (e) {
41
+ console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
42
+ }
43
+ }
44
+ const packageJsonPath = import_path.default.join(cwd, "package.json");
45
+ if (import_fs.default.existsSync(packageJsonPath)) {
46
+ try {
47
+ const raw = import_fs.default.readFileSync(packageJsonPath, "utf-8");
48
+ const pkg = JSON.parse(raw);
49
+ if (pkg.agentLens && typeof pkg.agentLens === "object") {
50
+ return pkg.agentLens;
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ return {};
56
+ }
57
+ function detectStartCwd(providedCwd) {
58
+ if (providedCwd) {
59
+ const resolved = import_path.default.resolve(process.cwd(), providedCwd);
60
+ if (import_fs.default.existsSync(resolved)) {
61
+ return providedCwd;
62
+ }
63
+ }
64
+ const rootPkgPath = import_path.default.join(process.cwd(), "package.json");
65
+ let rootHasDevScript = false;
66
+ if (import_fs.default.existsSync(rootPkgPath)) {
67
+ try {
68
+ const rootPkg = JSON.parse(import_fs.default.readFileSync(rootPkgPath, "utf-8"));
69
+ rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
70
+ } catch {
71
+ }
72
+ }
73
+ if (rootHasDevScript) {
74
+ return void 0;
75
+ }
76
+ const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
77
+ for (const candidate of candidates) {
78
+ const candidatePkg = import_path.default.join(process.cwd(), candidate, "package.json");
79
+ if (import_fs.default.existsSync(candidatePkg)) {
80
+ return `./${candidate}`;
81
+ }
82
+ }
83
+ return void 0;
84
+ }
85
+ function resolveWwwrootDir(customDir) {
86
+ if (customDir) {
87
+ return import_path.default.resolve(process.cwd(), customDir);
88
+ }
89
+ const candidates = [
90
+ "dist",
91
+ "build",
92
+ "out",
93
+ "wwwroot",
94
+ "Frontend/dist",
95
+ "frontend/dist",
96
+ "client/dist"
97
+ ];
98
+ for (const c of candidates) {
99
+ const candidatePath = import_path.default.resolve(process.cwd(), c);
100
+ if (import_fs.default.existsSync(candidatePath) && import_fs.default.existsSync(import_path.default.join(candidatePath, "index.html"))) {
101
+ return candidatePath;
102
+ }
103
+ }
104
+ for (const c of candidates) {
105
+ const candidatePath = import_path.default.resolve(process.cwd(), c);
106
+ if (import_fs.default.existsSync(candidatePath)) {
107
+ return candidatePath;
108
+ }
109
+ }
110
+ return import_path.default.resolve(process.cwd(), "dist");
111
+ }
29
112
 
30
113
  // src/features/runner/runner.ts
31
- var import_path6 = __toESM(require("path"));
32
- var import_fs6 = __toESM(require("fs"));
114
+ var import_path7 = __toESM(require("path"));
115
+ var import_fs7 = __toESM(require("fs"));
33
116
 
34
117
  // src/shared/api/dsl.ts
35
118
  var VIEWPORT_PRESETS = {
36
- /** Minimum supported window size (e.g., ) */
119
+ /** Minimum supported window size */
37
120
  MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
38
- /** Standard default window size (e.g., ) */
121
+ /** Standard default window size */
39
122
  DEFAULT: { name: "default", width: 1200, height: 800 },
40
123
  /** Wide screen for checking grids and tables */
41
124
  WIDE: { name: "wide", width: 1600, height: 900 },
@@ -47,16 +130,16 @@ function defineVisualTest(scenario) {
47
130
  }
48
131
 
49
132
  // src/features/capture/capture.ts
50
- var import_fs = __toESM(require("fs"));
51
- var import_path = __toESM(require("path"));
133
+ var import_fs2 = __toESM(require("fs"));
134
+ var import_path2 = __toESM(require("path"));
52
135
  var CaptureEngine = class {
53
136
  outputDir;
54
137
  currentStepIndex = 0;
55
138
  recordedSnapshots = [];
56
139
  constructor(outputDir) {
57
140
  this.outputDir = outputDir;
58
- if (!import_fs.default.existsSync(this.outputDir)) {
59
- import_fs.default.mkdirSync(this.outputDir, { recursive: true });
141
+ if (!import_fs2.default.existsSync(this.outputDir)) {
142
+ import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
60
143
  }
61
144
  }
62
145
  getSnapshots() {
@@ -67,17 +150,17 @@ var CaptureEngine = class {
67
150
  }
68
151
  /**
69
152
  */
70
- async takeSnapshot(page, name, viewport, options2) {
153
+ async takeSnapshot(page, name, viewport, options) {
71
154
  this.currentStepIndex += 1;
72
155
  const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
73
156
  const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
74
157
  const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
75
- const filePath = import_path.default.join(this.outputDir, fileName);
76
- if (options2?.selector) {
77
- const element = await page.waitForSelector(options2.selector, { timeout: 5e3 });
158
+ const filePath = import_path2.default.join(this.outputDir, fileName);
159
+ if (options?.selector) {
160
+ const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
78
161
  await element.screenshot({ path: filePath });
79
162
  } else {
80
- await page.screenshot({ path: filePath, fullPage: options2?.fullPage ?? false });
163
+ await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
81
164
  }
82
165
  const metadata = {
83
166
  index: this.currentStepIndex,
@@ -87,20 +170,20 @@ var CaptureEngine = class {
87
170
  relativeUri: `./${fileName}`,
88
171
  viewport,
89
172
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
90
- selector: options2?.selector
173
+ selector: options?.selector
91
174
  };
92
175
  this.recordedSnapshots.push(metadata);
93
176
  return metadata;
94
177
  }
95
178
  /**
96
179
  */
97
- async takeBurst(page, name, viewport, options2) {
180
+ async takeBurst(page, name, viewport, options) {
98
181
  this.currentStepIndex += 1;
99
182
  const stepIndex = this.currentStepIndex;
100
183
  const paddedIndex = String(stepIndex).padStart(2, "0");
101
184
  const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
102
- const duration = Math.max(options2.durationMs, 50);
103
- const interval = Math.max(options2.intervalMs ?? 80, 20);
185
+ const duration = Math.max(options.durationMs, 50);
186
+ const interval = Math.max(options.intervalMs ?? 80, 20);
104
187
  const totalFrames = Math.ceil(duration / interval);
105
188
  const burstSnapshots = [];
106
189
  const burstGroup = sanitizedName;
@@ -108,9 +191,9 @@ var CaptureEngine = class {
108
191
  const elapsedMs = frame * interval;
109
192
  const paddedFrame = String(frame + 1).padStart(2, "0");
110
193
  const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
111
- const filePath = import_path.default.join(this.outputDir, fileName);
112
- if (options2.selector) {
113
- const element = await page.$(options2.selector);
194
+ const filePath = import_path2.default.join(this.outputDir, fileName);
195
+ if (options.selector) {
196
+ const element = await page.$(options.selector);
114
197
  if (element) {
115
198
  await element.screenshot({ path: filePath });
116
199
  } else {
@@ -130,7 +213,7 @@ var CaptureEngine = class {
130
213
  isBurstFrame: true,
131
214
  burstGroup,
132
215
  frameIndex: frame + 1,
133
- selector: options2.selector
216
+ selector: options.selector
134
217
  };
135
218
  burstSnapshots.push(meta);
136
219
  this.recordedSnapshots.push(meta);
@@ -143,8 +226,8 @@ var CaptureEngine = class {
143
226
  };
144
227
 
145
228
  // src/features/reporter/reporter.ts
146
- var import_fs2 = __toESM(require("fs"));
147
- var import_path2 = __toESM(require("path"));
229
+ var import_fs3 = __toESM(require("fs"));
230
+ var import_path3 = __toESM(require("path"));
148
231
  var VisualReporter = class {
149
232
  static generateReport(data) {
150
233
  const {
@@ -156,8 +239,8 @@ var VisualReporter = class {
156
239
  targetMode,
157
240
  durationMs
158
241
  } = data;
159
- const manifestPath = import_path2.default.join(outputDir, "manifest.json");
160
- const reportPath = import_path2.default.join(outputDir, "report.md");
242
+ const manifestPath = import_path3.default.join(outputDir, "manifest.json");
243
+ const reportPath = import_path3.default.join(outputDir, "report.md");
161
244
  const manifest = {
162
245
  scenarioId: scenario.id,
163
246
  title: scenario.title,
@@ -172,7 +255,7 @@ var VisualReporter = class {
172
255
  errors: consoleErrors,
173
256
  warnings: consoleWarnings
174
257
  };
175
- import_fs2.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
258
+ import_fs3.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
176
259
  const rows = [];
177
260
  const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
178
261
  const burstGroups = /* @__PURE__ */ new Map();
@@ -182,9 +265,9 @@ var VisualReporter = class {
182
265
  burstGroups.set(s.burstGroup, list);
183
266
  });
184
267
  for (const snap of regularSnapshots) {
185
- const fileUri = `file:///${snap.filePath.replace(/\\/g, "/")}`;
268
+ const relativeLink = snap.relativeUri || `./${snap.fileName}`;
186
269
  rows.push(
187
- `| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${fileUri}) |`
270
+ `| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${relativeLink}) |`
188
271
  );
189
272
  }
190
273
  let burstSections = "";
@@ -201,8 +284,8 @@ var VisualReporter = class {
201
284
  | :--- | :--- | :--- | :--- |
202
285
  `;
203
286
  for (const frame of frames) {
204
- const fileUri = `file:///${frame.filePath.replace(/\\/g, "/")}`;
205
- burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${fileUri}) | ![](${fileUri}) |
287
+ const relativeLink = frame.relativeUri || `./${frame.fileName}`;
288
+ burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${relativeLink}) | ![](${relativeLink}) |
206
289
  `;
207
290
  }
208
291
  burstSections += `
@@ -265,6 +348,17 @@ ${err.stack}
265
348
  if (consoleWarnings.length > 20) {
266
349
  consoleSections += `
267
350
  *...and ${consoleWarnings.length - 20} more warnings (full list in manifest.json)*
351
+ `;
352
+ }
353
+ }
354
+ let pluginSections = "";
355
+ if (data.customSections && data.customSections.length > 0) {
356
+ for (const sec of data.customSections) {
357
+ pluginSections += `
358
+ ## ${sec.title}
359
+
360
+ ${sec.content}
361
+
268
362
  `;
269
363
  }
270
364
  }
@@ -287,18 +381,21 @@ ${rows.join("\n")}
287
381
 
288
382
  ${burstSections}
289
383
  ${consoleSections}
384
+ ${pluginSections}
290
385
  ---
291
386
 
292
387
  ## \u{1F4CB} AI Agent Verification Checklist:
293
388
  - [ ] **Console Errors**: ${consoleErrors.length === 0 ? "\u2705 No errors found" : `\u274C ${consoleErrors.length} errors \u2014 MUST REVIEW`}
389
+ - [ ] **Visual Layout Check**: Inspect snapshots (e.g. view_image on \`./01_${regularSnapshots[0]?.fileName || "quick_snap"}\`) for layout shifts or clipped elements.
294
390
  - [ ] **Responsiveness at \`1024x768\`**: Elements do not overflow the screen, no unwanted horizontal scroll.
391
+
295
392
  - [ ] **Typography & Spacing**: Spacing matches the design system and layout grids.
296
393
  - [ ] **Color Palette & Theme**: Background tints and button accent colors match the concept.
297
394
  - [ ] **Component States**: Modals open centered, dropdowns do not overlap with other layers (z-index).
298
395
  - [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
299
396
 
300
397
  `;
301
- import_fs2.default.writeFileSync(reportPath, reportContent, "utf-8");
398
+ import_fs3.default.writeFileSync(reportPath, reportContent, "utf-8");
302
399
  return reportPath;
303
400
  }
304
401
  static getHealthStatus(errors, warnings) {
@@ -316,8 +413,6 @@ ${consoleSections}
316
413
  var ConsoleTracker = class {
317
414
  entries = [];
318
415
  attached = false;
319
- /**
320
- */
321
416
  attach(page) {
322
417
  if (this.attached) return;
323
418
  this.attached = true;
@@ -386,8 +481,6 @@ var ConsoleTracker = class {
386
481
  return "log";
387
482
  }
388
483
  }
389
- /**
390
- */
391
484
  isIgnoredWarning(text) {
392
485
  const ignoredPatterns = [
393
486
  "findDOMNode is deprecated",
@@ -404,368 +497,32 @@ var ConsoleTracker = class {
404
497
  }
405
498
  };
406
499
 
407
- // src/shared/drivers/desktopDriver.ts
408
- var import_child_process = require("child_process");
500
+ // src/shared/drivers/previewDriver.ts
409
501
  var import_http = __toESM(require("http"));
410
- var import_path3 = __toESM(require("path"));
411
- var import_fs3 = __toESM(require("fs"));
502
+ var import_fs4 = __toESM(require("fs"));
503
+ var import_path4 = __toESM(require("path"));
412
504
 
413
505
  // src/shared/lib/playwrightLoader.ts
414
506
  var import_playwright = require("playwright");
415
-
416
- // src/shared/drivers/desktopDriver.ts
417
- var DesktopDriver = class {
418
- port;
419
- executablePath;
420
- autoLaunch;
421
- args;
422
- env;
423
- cwd;
424
- childProcess = null;
425
- browser = null;
426
- context = null;
427
- page = null;
428
- processStderr = "";
429
- processExited = false;
430
- exitCode = null;
431
- constructor(options2) {
432
- this.port = options2?.port || 9222;
433
- this.autoLaunch = options2?.autoLaunch ?? true;
434
- this.executablePath = options2?.executablePath;
435
- this.args = options2?.args || [];
436
- this.env = options2?.env || {};
437
- this.cwd = options2?.cwd;
438
- }
439
- async isPortAvailable() {
440
- return new Promise((resolve) => {
441
- const req = import_http.default.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
442
- resolve(res.statusCode === 200);
443
- });
444
- req.on("error", () => resolve(false));
445
- req.setTimeout(800, () => {
446
- req.destroy();
447
- resolve(false);
448
- });
449
- });
450
- }
451
- async waitForPort(timeoutMs = 25e3) {
452
- const startTime = Date.now();
453
- while (Date.now() - startTime < timeoutMs) {
454
- if (this.processExited) {
455
- throw new Error(
456
- `[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
457
- Stderr: ${this.processStderr.trim() || "(none)"}`
458
- );
459
- }
460
- if (await this.isPortAvailable()) {
461
- return;
462
- }
463
- await new Promise((r) => setTimeout(r, 400));
464
- }
465
- throw new Error(
466
- `Timeout waiting for WebView2/Chromium CDP port ${this.port}. Last stderr:
467
- ${this.processStderr.trim() || "(no stderr output)"}`
468
- );
469
- }
470
- async start(initialViewport = { width: 1200, height: 800 }) {
471
- const alreadyRunning = await this.isPortAvailable();
472
- if (!alreadyRunning) {
473
- if (!this.autoLaunch) {
474
- throw new Error(
475
- `App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
476
- );
477
- }
478
- if (!this.executablePath) {
479
- throw new Error(
480
- `[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
481
- );
482
- }
483
- const resolvedExe = import_path3.default.resolve(process.cwd(), this.executablePath);
484
- if (!import_fs3.default.existsSync(resolvedExe)) {
485
- throw new Error(
486
- `[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
487
- );
488
- }
489
- console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
490
- const mergedEnv = {
491
- ...process.env,
492
- ...this.env,
493
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${this.port}`
494
- };
495
- this.processStderr = "";
496
- this.processExited = false;
497
- this.exitCode = null;
498
- this.childProcess = (0, import_child_process.spawn)(resolvedExe, this.args, {
499
- env: mergedEnv,
500
- cwd: this.cwd || import_path3.default.dirname(resolvedExe),
501
- stdio: ["ignore", "ignore", "pipe"],
502
- detached: false
503
- });
504
- this.childProcess.stderr?.on("data", (chunk) => {
505
- this.processStderr += chunk.toString();
506
- });
507
- this.childProcess.on("exit", (code) => {
508
- this.processExited = true;
509
- this.exitCode = code;
510
- });
511
- this.childProcess.on("error", (err) => {
512
- console.error("[DesktopDriver] Failed to spawn process:", err);
513
- });
514
- console.log(`[DesktopDriver] Waiting for CDP debugging port on ${this.port}...`);
515
- await this.waitForPort();
516
- } else {
517
- console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
518
- }
519
- console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
520
- this.browser = await import_playwright.chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
521
- const contexts = this.browser.contexts();
522
- this.context = contexts[0] || await this.browser.newContext();
523
- const pages = this.context.pages();
524
- if (pages.length > 0) {
525
- this.page = pages[0];
526
- } else {
527
- this.page = await this.context.waitForEvent("page", { timeout: 1e4 });
528
- }
529
- if (initialViewport) {
530
- await this.page.setViewportSize(initialViewport).catch(() => {
531
- });
532
- }
533
- return { page: this.page, context: this.context, browser: this.browser };
534
- }
535
- async stop() {
536
- if (this.browser) {
537
- await this.browser.close().catch(() => {
538
- });
539
- this.browser = null;
540
- }
541
- if (this.childProcess && !this.childProcess.killed) {
542
- console.log("[DesktopDriver] Terminating spawned desktop process...");
543
- try {
544
- if (process.platform === "win32" && this.childProcess.pid) {
545
- (0, import_child_process.spawn)("taskkill", ["/pid", String(this.childProcess.pid), "/T", "/F"], { stdio: "ignore" });
546
- } else {
547
- this.childProcess.kill("SIGTERM");
548
- }
549
- } catch {
550
- }
551
- this.childProcess = null;
552
- }
553
- }
554
- };
555
-
556
- // src/shared/drivers/previewDriver.ts
557
- var import_http2 = __toESM(require("http"));
558
- var import_fs5 = __toESM(require("fs"));
559
- var import_path5 = __toESM(require("path"));
560
-
561
- // src/features/mock-ipc/mockIpc.ts
562
- var MockIpcRegistry = class {
563
- mocks = /* @__PURE__ */ new Map();
564
- set(action, data, options2) {
565
- this.mocks.set(action, {
566
- action,
567
- data,
568
- type: options2?.type ?? "SUCCESS",
569
- delayMs: options2?.delayMs ?? 20
570
- });
571
- }
572
- setBatch(entries) {
573
- for (const entry of entries) {
574
- this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
575
- }
576
- }
577
- remove(action) {
578
- this.mocks.delete(action);
579
- }
580
- clear() {
581
- this.mocks.clear();
582
- }
583
- get(action) {
584
- return this.mocks.get(action) ?? null;
585
- }
586
- toSerializable() {
587
- const result = {};
588
- for (const [action, entry] of this.mocks.entries()) {
589
- result[action] = {
590
- data: entry.data,
591
- type: entry.type ?? "SUCCESS",
592
- delayMs: entry.delayMs ?? 20
593
- };
594
- }
595
- return result;
596
- }
597
- get size() {
598
- return this.mocks.size;
599
- }
600
- };
601
- function generateMockIpcScript(registry) {
602
- const mocksJson = JSON.stringify(registry.toSerializable());
603
- return `
604
- (() => {
605
- const __mockTable = ${mocksJson};
606
-
607
- window.__visualRunnerMocks = __mockTable;
608
-
609
- // Generic IPC mock bridge for modern web applications
610
- window.__mockIpc = {
611
- invoke: (action, payload) => {
612
- return new Promise((resolve, reject) => {
613
- const mock = window.__visualRunnerMocks[action];
614
-
615
- if (mock) {
616
- setTimeout(() => {
617
- if (mock.type === 'ERROR') {
618
- reject(new Error(mock.data));
619
- } else {
620
- resolve(mock.data);
621
- }
622
- }, mock.delayMs || 20);
623
- } else {
624
- console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
625
- setTimeout(() => resolve(null), 20);
626
- }
627
- });
628
- }
629
- };
630
-
631
- // Legacy fallback for generic window.external
632
- if (!window.external) {
633
- window.external = {};
634
- }
635
-
636
- window.external.sendMessage = (msg) => {
507
+ var chromium = new Proxy(import_playwright.chromium, {
508
+ get(target, prop, receiver) {
509
+ if (prop === "launch") {
510
+ return async (...args) => {
637
511
  try {
638
- const parsed = JSON.parse(msg);
639
- const action = parsed.Action || parsed.action;
640
- const id = parsed.Id || parsed.id;
641
-
642
- const mock = window.__visualRunnerMocks[action];
643
-
644
- if (mock) {
645
- const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
646
- setTimeout(() => {
647
- const cb = window.__mockCallback;
648
- if (cb) cb(JSON.stringify(response));
649
- }, mock.delayMs || 20);
650
- } else {
651
- setTimeout(() => {
652
- const cb = window.__mockCallback;
653
- if (cb) cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
654
- }, 20);
512
+ return await target.launch(...args);
513
+ } catch (err) {
514
+ if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
515
+ console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
516
+ console.error("\u{1F449} Please install it by running: npx playwright install chromium\n");
655
517
  }
656
- } catch (e) {
657
- console.error('[Mock IPC] Failed to process legacy message:', e);
518
+ throw err;
658
519
  }
659
520
  };
660
-
661
- window.external.receiveMessage = (callback) => {
662
- window.__mockCallback = callback;
663
- };
664
-
665
- console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys(window.__visualRunnerMocks).length, 'mocked actions');
666
- })();
667
- `;
668
- }
669
-
670
- // src/shared/lib/config.ts
671
- var import_fs4 = __toESM(require("fs"));
672
- var import_path4 = __toESM(require("path"));
673
- function loadConfig(cwd = process.cwd()) {
674
- const configPath = import_path4.default.join(cwd, "agent-lens.json");
675
- if (import_fs4.default.existsSync(configPath)) {
676
- try {
677
- const raw = import_fs4.default.readFileSync(configPath, "utf8");
678
- return JSON.parse(raw);
679
- } catch (e) {
680
- console.warn(`\u26A0\uFE0F Warning: Failed to parse agent-lens.json:`, e);
681
- }
682
- }
683
- const pkgPath = import_path4.default.join(cwd, "package.json");
684
- if (import_fs4.default.existsSync(pkgPath)) {
685
- try {
686
- const raw = import_fs4.default.readFileSync(pkgPath, "utf8");
687
- const pkg = JSON.parse(raw);
688
- if (pkg.agentLens && typeof pkg.agentLens === "object") {
689
- return pkg.agentLens;
690
- }
691
- } catch {
692
- }
693
- }
694
- return {};
695
- }
696
- function resolveWwwrootDir(customPath, cwd = process.cwd()) {
697
- if (customPath) {
698
- return import_path4.default.resolve(cwd, customPath);
699
- }
700
- const standardDirs = [
701
- "dist",
702
- "build",
703
- "wwwroot",
704
- "Frontend/dist",
705
- "frontend/dist",
706
- "client/dist",
707
- "web/dist",
708
- "ui/dist"
709
- ];
710
- for (const rel of standardDirs) {
711
- const candidate = import_path4.default.resolve(cwd, rel);
712
- if (import_fs4.default.existsSync(candidate) && import_fs4.default.existsSync(import_path4.default.join(candidate, "index.html"))) {
713
- return candidate;
714
521
  }
522
+ const val = Reflect.get(target, prop, receiver);
523
+ return typeof val === "function" ? val.bind(target) : val;
715
524
  }
716
- const foundDeepWwwroot = findDeepIndexHtmlDir(cwd, 4);
717
- if (foundDeepWwwroot) {
718
- return foundDeepWwwroot;
719
- }
720
- for (const rel of standardDirs) {
721
- const candidate = import_path4.default.resolve(cwd, rel);
722
- if (import_fs4.default.existsSync(candidate)) {
723
- return candidate;
724
- }
725
- }
726
- return import_path4.default.resolve(cwd, "dist");
727
- }
728
- function detectStartCwd(customCwd, rootCwd = process.cwd()) {
729
- if (customCwd) {
730
- return import_path4.default.resolve(rootCwd, customCwd);
731
- }
732
- const subdirectories = ["Frontend", "frontend", "client", "web", "ui", "app"];
733
- for (const sub of subdirectories) {
734
- const subPkg = import_path4.default.join(rootCwd, sub, "package.json");
735
- if (import_fs4.default.existsSync(subPkg)) {
736
- try {
737
- const json = JSON.parse(import_fs4.default.readFileSync(subPkg, "utf8"));
738
- if (json.scripts && (json.scripts.dev || json.scripts.start || json.scripts.build)) {
739
- return import_path4.default.join(rootCwd, sub);
740
- }
741
- } catch {
742
- }
743
- }
744
- }
745
- return rootCwd;
746
- }
747
- function findDeepIndexHtmlDir(dir, maxDepth, currentDepth = 0) {
748
- if (currentDepth > maxDepth || !import_fs4.default.existsSync(dir)) return null;
749
- try {
750
- const entries = import_fs4.default.readdirSync(dir, { withFileTypes: true });
751
- for (const entry of entries) {
752
- if (entry.isDirectory()) {
753
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "artifacts") {
754
- continue;
755
- }
756
- const subDir = import_path4.default.join(dir, entry.name);
757
- if (entry.name === "wwwroot" && import_fs4.default.existsSync(import_path4.default.join(subDir, "index.html"))) {
758
- return subDir;
759
- }
760
- const found = findDeepIndexHtmlDir(subDir, maxDepth, currentDepth + 1);
761
- if (found) return found;
762
- }
763
- }
764
- } catch {
765
- return null;
766
- }
767
- return null;
768
- }
525
+ });
769
526
 
770
527
  // src/shared/drivers/previewDriver.ts
771
528
  var MIME_TYPES = {
@@ -791,13 +548,12 @@ var PreviewDriver = class {
791
548
  serverPort = 0;
792
549
  options;
793
550
  _baseUrl = "";
794
- mockRegistry;
795
- constructor(options2) {
796
- this.options = options2 || {};
551
+ initialRouteMocks = [];
552
+ constructor(options) {
553
+ this.options = options || {};
797
554
  if (!this.options.url) {
798
555
  this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
799
556
  }
800
- this.mockRegistry = new MockIpcRegistry();
801
557
  }
802
558
  get baseUrl() {
803
559
  return this._baseUrl;
@@ -805,25 +561,25 @@ var PreviewDriver = class {
805
561
  async start(initialViewport = { width: 1200, height: 800 }) {
806
562
  let targetUrl = this.options.url;
807
563
  if (!targetUrl) {
808
- if (!import_fs5.default.existsSync(this.options.wwwrootDir)) {
564
+ if (!import_fs4.default.existsSync(this.options.wwwrootDir)) {
809
565
  throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
810
566
  }
811
567
  await new Promise((resolve, reject) => {
812
- this.server = import_http2.default.createServer((req, res) => {
568
+ this.server = import_http.default.createServer((req, res) => {
813
569
  let reqUrl = req.url?.split("?")[0] || "/";
814
570
  if (reqUrl === "/") reqUrl = "/index.html";
815
- let safePath = import_path5.default.normalize(import_path5.default.join(this.options.wwwrootDir, reqUrl));
571
+ let safePath = import_path4.default.normalize(import_path4.default.join(this.options.wwwrootDir, reqUrl));
816
572
  if (!safePath.startsWith(this.options.wwwrootDir)) {
817
573
  res.writeHead(403);
818
574
  return res.end("Forbidden");
819
575
  }
820
- if (!import_fs5.default.existsSync(safePath) || import_fs5.default.statSync(safePath).isDirectory()) {
821
- safePath = import_path5.default.join(this.options.wwwrootDir, "index.html");
576
+ if (!import_fs4.default.existsSync(safePath) || import_fs4.default.statSync(safePath).isDirectory()) {
577
+ safePath = import_path4.default.join(this.options.wwwrootDir, "index.html");
822
578
  }
823
- const ext = import_path5.default.extname(safePath).toLowerCase();
579
+ const ext = import_path4.default.extname(safePath).toLowerCase();
824
580
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
825
581
  try {
826
- const content = import_fs5.default.readFileSync(safePath);
582
+ const content = import_fs4.default.readFileSync(safePath);
827
583
  res.writeHead(200, { "Content-Type": contentType });
828
584
  res.end(content);
829
585
  } catch (e) {
@@ -847,7 +603,7 @@ var PreviewDriver = class {
847
603
  this._baseUrl = targetUrl;
848
604
  console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
849
605
  }
850
- this.browser = await import_playwright.chromium.launch({
606
+ this.browser = await chromium.launch({
851
607
  headless: !this.options.headed,
852
608
  args: ["--no-sandbox", "--disable-setuid-sandbox"]
853
609
  });
@@ -855,35 +611,41 @@ var PreviewDriver = class {
855
611
  viewport: initialViewport,
856
612
  deviceScaleFactor: 1
857
613
  });
858
- if (this.mockRegistry.size > 0) {
859
- const mockScript = generateMockIpcScript(this.mockRegistry);
860
- await this.context.addInitScript(mockScript);
861
- }
862
614
  this.page = await this.context.newPage();
615
+ if (this.initialRouteMocks.length > 0) {
616
+ for (const entry of this.initialRouteMocks) {
617
+ await this.addRouteMock(entry);
618
+ }
619
+ }
863
620
  await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
864
621
  return { page: this.page, context: this.context, browser: this.browser };
865
622
  }
866
- async updateMockIpc(action, data, options2) {
623
+ async addRouteMock(entry) {
867
624
  if (!this.page) {
868
- throw new Error("PreviewDriver not started. Call start() first.");
625
+ this.initialRouteMocks.push(entry);
626
+ return;
869
627
  }
870
- this.mockRegistry.set(action, data, options2);
871
- await this.page.evaluate(
872
- ({ action: action2, mock }) => {
873
- if (!window.__visualRunnerMocks) {
874
- window.__visualRunnerMocks = {};
875
- }
876
- window.__visualRunnerMocks[action2] = mock;
877
- },
878
- {
879
- action,
880
- mock: {
881
- data,
882
- type: options2?.type ?? "SUCCESS",
883
- delayMs: options2?.delayMs ?? 20
884
- }
628
+ await this.page.route(entry.url, async (route) => {
629
+ const req = route.request();
630
+ if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
631
+ return route.continue();
885
632
  }
886
- );
633
+ if (entry.delayMs) {
634
+ await new Promise((r) => setTimeout(r, entry.delayMs));
635
+ }
636
+ const isJson = typeof entry.body === "object" && entry.body !== null;
637
+ await route.fulfill({
638
+ status: entry.status ?? 200,
639
+ contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
640
+ body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
641
+ headers: entry.headers
642
+ });
643
+ });
644
+ }
645
+ async setupRouteMocks(routes) {
646
+ for (const r of routes) {
647
+ await this.addRouteMock(r);
648
+ }
887
649
  }
888
650
  async stop() {
889
651
  if (this.context) {
@@ -906,133 +668,511 @@ var PreviewDriver = class {
906
668
  };
907
669
 
908
670
  // src/shared/lib/processManager.ts
909
- var import_child_process2 = require("child_process");
671
+ var import_child_process = require("child_process");
910
672
  var import_tree_kill = __toESM(require("tree-kill"));
911
673
  var ProcessManager = class {
912
- child = null;
913
- stderrOutput = "";
914
- stdoutOutput = "";
915
- hasExited = false;
916
- exitCode = null;
917
- /**
918
- * Spawns a background process (e.g. dev server, backend, or app)
919
- */
920
- async start(command, options2) {
921
- this.stderrOutput = "";
922
- this.stdoutOutput = "";
923
- this.hasExited = false;
924
- this.exitCode = null;
925
- const cwd = options2?.cwd || process.cwd();
926
- const env = { ...process.env, ...options2?.env };
674
+ childProcess = null;
675
+ isStopped = false;
676
+ async start(command, options) {
677
+ const cwd = options?.cwd || process.cwd();
927
678
  console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
928
- this.child = (0, import_child_process2.spawn)(command, {
679
+ this.childProcess = (0, import_child_process.spawn)(command, {
929
680
  cwd,
930
- env,
931
- shell: options2?.shell ?? true,
681
+ env: { ...process.env, ...options?.env },
682
+ shell: true,
932
683
  stdio: ["ignore", "pipe", "pipe"]
933
684
  });
934
- this.child.stdout?.on("data", (chunk) => {
935
- const str = chunk.toString();
936
- this.stdoutOutput += str;
937
- });
938
- this.child.stderr?.on("data", (chunk) => {
939
- const str = chunk.toString();
940
- this.stderrOutput += str;
685
+ this.childProcess.stdout?.on("data", (chunk) => {
686
+ const line = chunk.toString().trim();
687
+ if (line) {
688
+ }
941
689
  });
942
- this.child.on("exit", (code) => {
943
- this.hasExited = true;
944
- this.exitCode = code;
690
+ this.childProcess.stderr?.on("data", (chunk) => {
691
+ const line = chunk.toString().trim();
692
+ if (line && !line.includes("ExperimentalWarning")) {
693
+ }
945
694
  });
946
- this.child.on("error", (err) => {
947
- console.error(`\u274C [ProcessManager] Failed to start command: "${command}":`, err.message);
695
+ this.childProcess.on("exit", (code, signal) => {
696
+ if (!this.isStopped && code !== 0 && code !== null) {
697
+ console.warn(`\u26A0\uFE0F [ProcessManager] Subprocess exited prematurely with code ${code}, signal ${signal}`);
698
+ }
948
699
  });
949
700
  }
950
- /**
951
- * Polls a URL until it starts responding or until timeout is reached.
952
- */
953
701
  async waitForUrl(url, timeoutMs = 3e4) {
702
+ console.log(`\u23F3 [ProcessManager] Waiting for ${url} to respond...`);
954
703
  const startTime = Date.now();
955
- console.log(`\u23F3 [ProcessManager] Waiting for URL to become available: ${url} (timeout: ${timeoutMs / 1e3}s)...`);
956
704
  while (Date.now() - startTime < timeoutMs) {
957
- if (this.hasExited && this.exitCode !== 0) {
705
+ if (this.childProcess && this.childProcess.exitCode !== null) {
958
706
  throw new Error(
959
- `[ProcessManager] Process exited prematurely with code ${this.exitCode}.
960
- Stderr:
961
- ${this.stderrOutput.trim() || "(no stderr)"}
962
- Stdout:
963
- ${this.stdoutOutput.slice(-500).trim()}`
707
+ `[ProcessManager] Server process exited with code ${this.childProcess.exitCode} while waiting for ${url}`
964
708
  );
965
709
  }
966
710
  try {
967
- const response = await fetch(url, { method: "GET", signal: AbortSignal.timeout(2e3) });
711
+ const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
968
712
  if (response.status) {
969
- console.log(`\u2705 [ProcessManager] Server responded with status ${response.status} at ${url}`);
970
- return;
713
+ console.log(`\u2705 [ProcessManager] Target ${url} is ready (status: ${response.status})!`);
714
+ return true;
971
715
  }
972
716
  } catch {
973
717
  }
974
- await new Promise((resolve) => setTimeout(resolve, 350));
718
+ await new Promise((r) => setTimeout(r, 500));
975
719
  }
976
- throw new Error(
977
- `[ProcessManager] Timeout (${timeoutMs / 1e3}s) waiting for server at ${url}.
978
- Last stdout:
979
- ${this.stdoutOutput.slice(-500).trim()}
980
- Last stderr:
981
- ${this.stderrOutput.trim()}`
982
- );
720
+ throw new Error(`[ProcessManager] Timeout after ${timeoutMs}ms waiting for ${url} to respond.`);
983
721
  }
984
- /**
985
- * Gracefully and forcefully kills the process and all its children.
986
- */
987
722
  async stop() {
988
- if (!this.child || !this.child.pid || this.hasExited) {
989
- this.child = null;
723
+ if (this.isStopped || !this.childProcess || !this.childProcess.pid) {
990
724
  return;
991
725
  }
992
- const pid = this.child.pid;
993
- console.log(`\u{1F6D1} [ProcessManager] Terminating process tree (PID: ${pid})...`);
726
+ this.isStopped = true;
727
+ const pid = this.childProcess.pid;
728
+ console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
994
729
  await new Promise((resolve) => {
995
- (0, import_tree_kill.default)(pid, "SIGKILL", (err) => {
730
+ (0, import_tree_kill.default)(pid, "SIGTERM", (err) => {
996
731
  if (err) {
997
- if (process.platform === "win32") {
998
- try {
999
- (0, import_child_process2.spawn)("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1000
- } catch {
1001
- }
732
+ try {
733
+ (0, import_tree_kill.default)(pid, "SIGKILL");
734
+ } catch {
1002
735
  }
1003
736
  }
1004
737
  resolve();
1005
738
  });
1006
739
  });
1007
- this.child = null;
740
+ this.childProcess = null;
1008
741
  }
1009
742
  };
1010
743
 
1011
- // src/features/runner/runner.ts
1012
- async function runVisualScenario(options2) {
1013
- const { scenario } = options2;
1014
- const targetMode = options2.targetMode || "preview";
1015
- const startTime = Date.now();
1016
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1017
- const artifactsRoot = options2.artifactsRoot || import_path6.default.resolve(process.cwd(), "artifacts");
1018
- if (options2.cleanArtifacts && import_fs6.default.existsSync(artifactsRoot)) {
1019
- console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
1020
- const entries = import_fs6.default.readdirSync(artifactsRoot, { withFileTypes: true });
1021
- for (const entry of entries) {
1022
- import_fs6.default.rmSync(import_path6.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
744
+ // src/shared/lib/pluginLoader.ts
745
+ var import_path5 = __toESM(require("path"));
746
+ var import_fs5 = __toESM(require("fs"));
747
+ var import_jiti = require("jiti");
748
+ function findPackageRoot() {
749
+ let cur = __dirname;
750
+ while (cur !== import_path5.default.dirname(cur)) {
751
+ const pkgPath = import_path5.default.join(cur, "package.json");
752
+ if (import_fs5.default.existsSync(pkgPath)) {
753
+ try {
754
+ const pkg = JSON.parse(import_fs5.default.readFileSync(pkgPath, "utf-8"));
755
+ if (pkg.name === "@_deep4wee/agent-lens" || pkg.name === "agent-lens") {
756
+ return cur;
757
+ }
758
+ } catch {
759
+ }
1023
760
  }
761
+ cur = import_path5.default.dirname(cur);
1024
762
  }
1025
- const scenarioArtifactsDir = import_path6.default.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1026
- if (!import_fs6.default.existsSync(scenarioArtifactsDir)) {
1027
- import_fs6.default.mkdirSync(scenarioArtifactsDir, { recursive: true });
763
+ return import_path5.default.resolve(__dirname, "..");
764
+ }
765
+ var PluginManager = class {
766
+ plugins = [];
767
+ jiti = (0, import_jiti.createJiti)(process.cwd());
768
+ register(plugin) {
769
+ if (this.plugins.some((p) => p.name === plugin.name)) {
770
+ return;
771
+ }
772
+ this.plugins.push(plugin);
773
+ console.log(`\u{1F50C} [Plugin] Registered: ${plugin.name}${plugin.version ? ` (v${plugin.version})` : ""}`);
774
+ }
775
+ async load(pluginSpec) {
776
+ if (typeof pluginSpec === "object" && pluginSpec !== null) {
777
+ this.register(pluginSpec);
778
+ return;
779
+ }
780
+ const pluginNameOrPath = pluginSpec.trim();
781
+ if (!pluginNameOrPath) return;
782
+ if (pluginNameOrPath.startsWith(".") || pluginNameOrPath.startsWith("/") || pluginNameOrPath.includes("/") || pluginNameOrPath.includes("\\")) {
783
+ const resolvedPath = import_path5.default.resolve(process.cwd(), pluginNameOrPath);
784
+ await this.loadFromFile(resolvedPath);
785
+ return;
786
+ }
787
+ const localCandidates = [
788
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.ts`),
789
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.js`),
790
+ import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.ts`),
791
+ import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.js`),
792
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.ts"),
793
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.js"),
794
+ import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.ts"),
795
+ import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.js")
796
+ ];
797
+ for (const candidate of localCandidates) {
798
+ if (import_fs5.default.existsSync(candidate)) {
799
+ await this.loadFromFile(candidate);
800
+ return;
801
+ }
802
+ }
803
+ const pkgRoot = findPackageRoot();
804
+ const builtInCandidates = [
805
+ import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.ts"),
806
+ import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.js"),
807
+ import_path5.default.join(pkgRoot, "dist", "plugins", pluginNameOrPath, "index.js"),
808
+ import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.ts"),
809
+ import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.js"),
810
+ import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.ts"),
811
+ import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.js"),
812
+ import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.ts"),
813
+ import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.js")
814
+ ];
815
+ for (const builtIn of builtInCandidates) {
816
+ if (import_fs5.default.existsSync(builtIn)) {
817
+ await this.loadFromFile(builtIn);
818
+ return;
819
+ }
820
+ }
821
+ try {
822
+ const mod = await this.jiti.import(pluginNameOrPath);
823
+ const plugin = mod.default || mod.plugin || mod;
824
+ if (plugin && plugin.name) {
825
+ this.register(plugin);
826
+ } else {
827
+ console.warn(`\u26A0\uFE0F [Plugin] Package "${pluginNameOrPath}" did not export a valid AgentLensPlugin.`);
828
+ }
829
+ } catch (err) {
830
+ console.warn(`\u26A0\uFE0F [Plugin] Could not load plugin '${pluginNameOrPath}': ${err.message}`);
831
+ }
832
+ }
833
+ async loadFromFile(filePath) {
834
+ if (!import_fs5.default.existsSync(filePath)) {
835
+ console.warn(`\u26A0\uFE0F [Plugin] Plugin file not found: ${filePath}`);
836
+ return;
837
+ }
838
+ try {
839
+ const mod = await this.jiti.import(filePath);
840
+ const plugin = mod.default || mod.plugin || mod;
841
+ if (plugin && plugin.name) {
842
+ this.register(plugin);
843
+ } else {
844
+ console.warn(`\u26A0\uFE0F [Plugin] File "${filePath}" does not export a valid AgentLensPlugin by default.`);
845
+ }
846
+ } catch (err) {
847
+ console.error(`\u274C [Plugin] Failed to import plugin from ${filePath}:`, err.message);
848
+ }
849
+ }
850
+ async loadAll(specs) {
851
+ for (const spec of specs) {
852
+ await this.load(spec);
853
+ }
854
+ }
855
+ async runSetup(hookContext) {
856
+ for (const p of this.plugins) {
857
+ if (p.setup) await p.setup(hookContext);
858
+ }
859
+ }
860
+ async launchSession(options, hookContext) {
861
+ for (const p of this.plugins) {
862
+ if (p.launchSession) {
863
+ const session = await p.launchSession(options, hookContext);
864
+ if (session) {
865
+ return session;
866
+ }
867
+ }
868
+ }
869
+ return void 0;
870
+ }
871
+ async runOnContextCreated(context, hookContext) {
872
+ for (const p of this.plugins) {
873
+ if (p.onContextCreated) await p.onContextCreated(context, hookContext);
874
+ }
875
+ }
876
+ async runOnPageCreated(page, context, hookContext) {
877
+ for (const p of this.plugins) {
878
+ if (p.onPageCreated) await p.onPageCreated(page, context, hookContext);
879
+ }
880
+ }
881
+ async extendContext(ctx, page, hookContext) {
882
+ for (const p of this.plugins) {
883
+ if (p.extendContext) {
884
+ const extensions = await p.extendContext(ctx, page, hookContext);
885
+ if (extensions && typeof extensions === "object") {
886
+ Object.assign(ctx, extensions);
887
+ }
888
+ }
889
+ }
890
+ }
891
+ async runOnAfterRun(reportData, hookContext) {
892
+ for (const p of this.plugins) {
893
+ if (p.onAfterRun) await p.onAfterRun(reportData, hookContext);
894
+ }
895
+ }
896
+ async runTeardown(hookContext) {
897
+ for (const p of this.plugins) {
898
+ if (p.teardown) {
899
+ try {
900
+ await p.teardown(hookContext);
901
+ } catch (e) {
902
+ console.error(`\u26A0\uFE0F [Plugin] Error in ${p.name}.teardown:`, e.message);
903
+ }
904
+ }
905
+ }
906
+ }
907
+ get loadedPlugins() {
908
+ return [...this.plugins];
909
+ }
910
+ };
911
+
912
+ // src/features/runner/lib/navigation.ts
913
+ function createNavigator(page, baseUrl, targetUrl) {
914
+ return async (route) => {
915
+ if (route.startsWith("http://") || route.startsWith("https://")) {
916
+ await page.goto(route, { waitUntil: "domcontentloaded" });
917
+ await page.waitForTimeout(300);
918
+ return;
919
+ }
920
+ if (route.startsWith("#")) {
921
+ await page.evaluate((r) => {
922
+ window.location.hash = r;
923
+ }, route);
924
+ await page.waitForTimeout(300);
925
+ return;
926
+ }
927
+ if (baseUrl && targetUrl) {
928
+ try {
929
+ const fullUrl = new URL(route, baseUrl).toString();
930
+ await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
931
+ await page.waitForTimeout(300);
932
+ return;
933
+ } catch {
934
+ }
935
+ }
936
+ const routePath = route.startsWith("/") ? route : `/${route}`;
937
+ await page.evaluate((r) => {
938
+ if (window.location.hash !== void 0) {
939
+ window.location.hash = r;
940
+ }
941
+ }, routePath);
942
+ await page.waitForTimeout(300);
943
+ };
944
+ }
945
+
946
+ // src/features/runner/lib/artifactsSync.ts
947
+ var import_fs6 = __toESM(require("fs"));
948
+ var import_path6 = __toESM(require("path"));
949
+ function syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots) {
950
+ try {
951
+ const latestDir = import_path6.default.join(artifactsRoot, "latest");
952
+ if (import_fs6.default.existsSync(latestDir)) {
953
+ import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
954
+ }
955
+ import_fs6.default.mkdirSync(latestDir, { recursive: true });
956
+ if (import_fs6.default.existsSync(reportPath)) {
957
+ import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
958
+ }
959
+ const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
960
+ if (import_fs6.default.existsSync(manifestSrc)) {
961
+ import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
962
+ }
963
+ for (const snap of snapshots) {
964
+ if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
965
+ import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
966
+ }
967
+ }
968
+ return import_path6.default.join(latestDir, "report.md");
969
+ } catch {
970
+ return void 0;
971
+ }
972
+ }
973
+
974
+ // src/features/runner/lib/contextBuilder.ts
975
+ function buildTestContext(options) {
976
+ const {
977
+ page,
978
+ context,
979
+ targetMode,
980
+ initialViewport,
981
+ captureEngine,
982
+ consoleTracker,
983
+ previewDriver,
984
+ doNavigate
985
+ } = options;
986
+ let currentViewport = { ...initialViewport };
987
+ const ctx = {
988
+ page,
989
+ context,
990
+ targetMode,
991
+ currentViewport,
992
+ // --- Snapshots ---
993
+ capture: async (name, opts) => {
994
+ console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
995
+ return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
996
+ },
997
+ captureBurst: async (name, opts) => {
998
+ console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
999
+ return await captureEngine.takeBurst(page, name, currentViewport, opts);
1000
+ },
1001
+ // --- Navigation ---
1002
+ navigate: async (route) => {
1003
+ console.log(`\u{1F9ED} [Navigate] ${route}`);
1004
+ await doNavigate(route);
1005
+ },
1006
+ // --- Viewport Management ---
1007
+ resize: async (width, height) => {
1008
+ console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1009
+ currentViewport = { width, height };
1010
+ ctx.currentViewport = currentViewport;
1011
+ await page.setViewportSize(currentViewport);
1012
+ await page.waitForTimeout(200);
1013
+ },
1014
+ setPreset: async (preset) => {
1015
+ console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1016
+ await ctx.resize(preset.width, preset.height);
1017
+ },
1018
+ resizeToFit: async (selector, padding = 0) => {
1019
+ let boundingBox;
1020
+ if (selector) {
1021
+ const el = await page.$(selector);
1022
+ if (el) {
1023
+ boundingBox = await el.boundingBox();
1024
+ }
1025
+ } else {
1026
+ boundingBox = await page.evaluate(() => ({
1027
+ width: document.documentElement.scrollWidth,
1028
+ height: document.documentElement.scrollHeight
1029
+ }));
1030
+ }
1031
+ if (boundingBox) {
1032
+ const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1033
+ const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1034
+ console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1035
+ await ctx.resize(newWidth, newHeight);
1036
+ } else {
1037
+ console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1038
+ }
1039
+ },
1040
+ // --- Waiting & DOM Observation ---
1041
+ wait: async (ms) => {
1042
+ await page.waitForTimeout(ms);
1043
+ },
1044
+ waitForSelector: async (selector, timeoutMs = 5e3) => {
1045
+ await page.waitForSelector(selector, { timeout: timeoutMs });
1046
+ },
1047
+ // --- Interaction ---
1048
+ click: async (selector) => {
1049
+ console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1050
+ await page.click(selector);
1051
+ },
1052
+ rightClick: async (selector) => {
1053
+ console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1054
+ await page.click(selector, { button: "right" });
1055
+ },
1056
+ type: async (selector, text) => {
1057
+ console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1058
+ await page.fill(selector, text);
1059
+ },
1060
+ selectOption: async (selector, value) => {
1061
+ console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1062
+ await page.selectOption(selector, value);
1063
+ },
1064
+ hover: async (selector) => {
1065
+ console.log(`\u{1F446} [Hover] ${selector}`);
1066
+ await page.hover(selector);
1067
+ },
1068
+ scroll: async (selector, deltaY) => {
1069
+ console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1070
+ await page.evaluate(
1071
+ ({ sel, dY }) => {
1072
+ const el = document.querySelector(sel);
1073
+ if (el) {
1074
+ el.scrollTop += dY;
1075
+ } else {
1076
+ window.scrollBy(0, dY);
1077
+ }
1078
+ },
1079
+ { sel: selector, dY: deltaY }
1080
+ );
1081
+ await page.waitForTimeout(100);
1082
+ },
1083
+ // --- Logging ---
1084
+ log: (msg) => {
1085
+ console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1086
+ },
1087
+ // --- Mock IPC (delegated or stubbed if plugin not loaded) ---
1088
+ setMockIpc: async (action, data, mockOptions) => {
1089
+ console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc called for "${action}". Ensure --plugin=mock-ipc is enabled.`);
1090
+ },
1091
+ // --- HTTP Route Mocking ---
1092
+ setMockRoute: async (url, body, routeOptions) => {
1093
+ if (!previewDriver) {
1094
+ console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored (no preview driver running)`);
1095
+ return;
1096
+ }
1097
+ console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
1098
+ await previewDriver.addRouteMock({
1099
+ url,
1100
+ body,
1101
+ method: routeOptions?.method,
1102
+ status: routeOptions?.status,
1103
+ delayMs: routeOptions?.delayMs,
1104
+ headers: routeOptions?.headers
1105
+ });
1106
+ },
1107
+ // --- Console Errors ---
1108
+ getConsoleErrors: () => consoleTracker.getErrors(),
1109
+ getConsoleWarnings: () => consoleTracker.getWarnings(),
1110
+ hasConsoleErrors: () => consoleTracker.hasErrors,
1111
+ // --- DOM Assertions ---
1112
+ readText: async (selector) => {
1113
+ const text = await page.textContent(selector);
1114
+ return text ? text.trim() : null;
1115
+ },
1116
+ getPageText: async () => {
1117
+ return await page.evaluate(() => document.body.innerText || "");
1118
+ },
1119
+ isVisible: async (selector) => {
1120
+ try {
1121
+ const element = await page.$(selector);
1122
+ if (!element) return false;
1123
+ return await element.isVisible();
1124
+ } catch {
1125
+ return false;
1126
+ }
1127
+ },
1128
+ getElementCount: async (selector) => {
1129
+ const elements = await page.$$(selector);
1130
+ return elements.length;
1131
+ }
1132
+ };
1133
+ return {
1134
+ ctx,
1135
+ getCurrentViewport: () => currentViewport
1136
+ };
1137
+ }
1138
+
1139
+ // src/features/runner/runner.ts
1140
+ async function runVisualScenario(options) {
1141
+ const { scenario } = options;
1142
+ const targetMode = options.targetMode || "preview";
1143
+ const startTime = Date.now();
1144
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1145
+ const artifactsRoot = options.artifactsRoot || import_path7.default.resolve(process.cwd(), "artifacts");
1146
+ if (options.cleanArtifacts && import_fs7.default.existsSync(artifactsRoot)) {
1147
+ console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
1148
+ const entries = import_fs7.default.readdirSync(artifactsRoot, { withFileTypes: true });
1149
+ for (const entry of entries) {
1150
+ import_fs7.default.rmSync(import_path7.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
1151
+ }
1152
+ }
1153
+ const scenarioArtifactsDir = import_path7.default.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1154
+ if (!import_fs7.default.existsSync(scenarioArtifactsDir)) {
1155
+ import_fs7.default.mkdirSync(scenarioArtifactsDir, { recursive: true });
1028
1156
  }
1029
1157
  const captureEngine = new CaptureEngine(scenarioArtifactsDir);
1030
1158
  const consoleTracker = new ConsoleTracker();
1031
1159
  const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
1032
- let currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1033
- let desktopDriver = null;
1160
+ const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1034
1161
  let previewDriver = null;
1035
1162
  let processManager = null;
1163
+ let customDriverStop = null;
1164
+ const pluginManager = new PluginManager();
1165
+ const pluginSpecs = [...options.plugins || [], ...scenario.plugins || []];
1166
+ if (pluginSpecs.length > 0) {
1167
+ await pluginManager.loadAll(pluginSpecs);
1168
+ }
1169
+ const hookContext = {
1170
+ scenario,
1171
+ targetMode,
1172
+ artifactsDir: scenarioArtifactsDir,
1173
+ cliOptions: options,
1174
+ state: /* @__PURE__ */ new Map()
1175
+ };
1036
1176
  try {
1037
1177
  let page;
1038
1178
  let context;
@@ -1043,211 +1183,69 @@ async function runVisualScenario(options2) {
1043
1183
  console.log(`\u{1F4C1} Artifacts: ${scenarioArtifactsDir}`);
1044
1184
  console.log(`========================================
1045
1185
  `);
1046
- if (options2.startCommand) {
1186
+ if (options.startCommand) {
1047
1187
  processManager = new ProcessManager();
1048
- await processManager.start(options2.startCommand, { cwd: options2.startCwd });
1049
- const waitTarget = options2.url || "http://localhost:5173";
1188
+ await processManager.start(options.startCommand, { cwd: options.startCwd });
1189
+ const waitTarget = options.url || "http://localhost:5173";
1050
1190
  await processManager.waitForUrl(waitTarget);
1051
1191
  }
1192
+ await pluginManager.runSetup(hookContext);
1052
1193
  if (typeof scenario.setup === "function") {
1053
1194
  console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
1054
1195
  await scenario.setup();
1055
1196
  }
1056
- if (targetMode === "desktop") {
1057
- desktopDriver = new DesktopDriver({
1058
- port: options2.port || 9222,
1059
- autoLaunch: options2.autoLaunchDesktop ?? true,
1060
- executablePath: options2.executablePath,
1061
- args: options2.desktopArgs,
1062
- env: options2.desktopEnv
1063
- });
1064
- const res = await desktopDriver.start(currentViewport);
1065
- page = res.page;
1066
- context = res.context;
1197
+ const customSession = await pluginManager.launchSession(
1198
+ { currentViewport, headed: options.headed },
1199
+ hookContext
1200
+ );
1201
+ if (customSession) {
1202
+ page = customSession.page;
1203
+ context = customSession.context;
1204
+ if (customSession.stop) {
1205
+ customDriverStop = customSession.stop;
1206
+ }
1207
+ } else if (targetMode === "desktop") {
1208
+ throw new Error(
1209
+ `[AgentLens] Target mode is 'desktop', but no desktop plugin is registered. Please add --plugin=desktop-webview2 to your command or configuration.`
1210
+ );
1067
1211
  } else {
1068
1212
  previewDriver = new PreviewDriver({
1069
- wwwrootDir: options2.wwwrootDir,
1070
- url: options2.url,
1071
- headed: options2.headed
1213
+ wwwrootDir: options.wwwrootDir,
1214
+ url: options.url,
1215
+ headed: options.headed
1072
1216
  });
1073
- const mergedMocks = [...options2.globalMocks || [], ...scenario.mockIpc || []];
1074
- if (mergedMocks.length > 0) {
1075
- console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
1076
- previewDriver.mockRegistry.setBatch(mergedMocks);
1217
+ if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1218
+ console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
1219
+ await previewDriver.setupRouteMocks(scenario.mockRoutes);
1077
1220
  }
1078
1221
  const res = await previewDriver.start(currentViewport);
1079
1222
  page = res.page;
1080
1223
  context = res.context;
1224
+ if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1225
+ await previewDriver.setupRouteMocks(scenario.mockRoutes);
1226
+ }
1081
1227
  }
1228
+ await pluginManager.runOnContextCreated(context, hookContext);
1229
+ await pluginManager.runOnPageCreated(page, context, hookContext);
1082
1230
  consoleTracker.attach(page);
1083
1231
  console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
1084
1232
  `);
1085
- const doNavigate = async (route) => {
1086
- if (route.startsWith("http://") || route.startsWith("https://")) {
1087
- await page.goto(route, { waitUntil: "domcontentloaded" });
1088
- await page.waitForTimeout(300);
1089
- return;
1090
- }
1091
- if (route.startsWith("#")) {
1092
- await page.evaluate((r) => {
1093
- window.location.hash = r;
1094
- }, route);
1095
- await page.waitForTimeout(300);
1096
- return;
1097
- }
1098
- if (previewDriver?.baseUrl && options2.url) {
1099
- try {
1100
- const fullUrl = new URL(route, previewDriver.baseUrl).toString();
1101
- await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
1102
- await page.waitForTimeout(300);
1103
- return;
1104
- } catch {
1105
- }
1106
- }
1107
- const routePath = route.startsWith("/") ? route : `/${route}`;
1108
- await page.evaluate((r) => {
1109
- if (window.location.hash !== void 0) {
1110
- window.location.hash = r;
1111
- }
1112
- }, routePath);
1113
- await page.waitForTimeout(300);
1114
- };
1233
+ const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
1115
1234
  if (scenario.route) {
1116
- console.log(`[Runner] Navigating to route: ${scenario.route}`);
1235
+ console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
1117
1236
  await doNavigate(scenario.route);
1118
1237
  }
1119
- const ctx = {
1238
+ const { ctx } = buildTestContext({
1120
1239
  page,
1121
1240
  context,
1122
1241
  targetMode,
1123
- currentViewport,
1124
- capture: async (name, opts) => {
1125
- console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
1126
- return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
1127
- },
1128
- captureBurst: async (name, opts) => {
1129
- console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
1130
- return await captureEngine.takeBurst(page, name, currentViewport, opts);
1131
- },
1132
- navigate: async (route) => {
1133
- console.log(`\u{1F9ED} [Navigate] ${route}`);
1134
- await doNavigate(route);
1135
- },
1136
- // ─── Viewport ───
1137
- resize: async (width, height) => {
1138
- console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1139
- currentViewport = { width, height };
1140
- ctx.currentViewport = currentViewport;
1141
- await page.setViewportSize(currentViewport);
1142
- await page.waitForTimeout(200);
1143
- },
1144
- setPreset: async (preset) => {
1145
- console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1146
- await ctx.resize(preset.width, preset.height);
1147
- },
1148
- resizeToFit: async (selector, padding = 0) => {
1149
- let boundingBox;
1150
- if (selector) {
1151
- const el = await page.$(selector);
1152
- if (el) {
1153
- boundingBox = await el.boundingBox();
1154
- }
1155
- } else {
1156
- boundingBox = await page.evaluate(() => {
1157
- return {
1158
- width: document.documentElement.scrollWidth,
1159
- height: document.documentElement.scrollHeight
1160
- };
1161
- });
1162
- }
1163
- if (boundingBox) {
1164
- const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1165
- const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1166
- console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1167
- await ctx.resize(newWidth, newHeight);
1168
- } else {
1169
- console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1170
- }
1171
- },
1172
- wait: async (ms) => {
1173
- await page.waitForTimeout(ms);
1174
- },
1175
- waitForSelector: async (selector, timeoutMs = 5e3) => {
1176
- await page.waitForSelector(selector, { timeout: timeoutMs });
1177
- },
1178
- click: async (selector) => {
1179
- console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1180
- await page.click(selector);
1181
- },
1182
- rightClick: async (selector) => {
1183
- console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1184
- await page.click(selector, { button: "right" });
1185
- },
1186
- type: async (selector, text) => {
1187
- console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1188
- await page.fill(selector, text);
1189
- },
1190
- selectOption: async (selector, value) => {
1191
- console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1192
- await page.selectOption(selector, value);
1193
- },
1194
- hover: async (selector) => {
1195
- console.log(`\u{1F446} [Hover] ${selector}`);
1196
- await page.hover(selector);
1197
- },
1198
- scroll: async (selector, deltaY) => {
1199
- console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1200
- await page.evaluate(({ sel, dY }) => {
1201
- const el = document.querySelector(sel);
1202
- if (el) {
1203
- el.scrollTop += dY;
1204
- } else {
1205
- window.scrollBy(0, dY);
1206
- }
1207
- }, { sel: selector, dY: deltaY });
1208
- await page.waitForTimeout(100);
1209
- },
1210
- log: (msg) => {
1211
- console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1212
- },
1213
- // ─── Mock IPC ───
1214
- setMockIpc: async (action, data, mockOptions) => {
1215
- if (targetMode === "desktop") {
1216
- console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
1217
- return;
1218
- }
1219
- if (!previewDriver) {
1220
- console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
1221
- return;
1222
- }
1223
- console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
1224
- await previewDriver.updateMockIpc(action, data, mockOptions);
1225
- },
1226
- getConsoleErrors: () => consoleTracker.getErrors(),
1227
- getConsoleWarnings: () => consoleTracker.getWarnings(),
1228
- hasConsoleErrors: () => consoleTracker.hasErrors,
1229
- // ─── DOM Assertions ───
1230
- readText: async (selector) => {
1231
- const text = await page.textContent(selector);
1232
- return text ? text.trim() : null;
1233
- },
1234
- getPageText: async () => {
1235
- return await page.evaluate(() => document.body.innerText || "");
1236
- },
1237
- isVisible: async (selector) => {
1238
- try {
1239
- const element = await page.$(selector);
1240
- if (!element) return false;
1241
- return await element.isVisible();
1242
- } catch {
1243
- return false;
1244
- }
1245
- },
1246
- getElementCount: async (selector) => {
1247
- const elements = await page.$$(selector);
1248
- return elements.length;
1249
- }
1250
- };
1242
+ initialViewport: currentViewport,
1243
+ captureEngine,
1244
+ consoleTracker,
1245
+ previewDriver,
1246
+ doNavigate
1247
+ });
1248
+ await pluginManager.extendContext(ctx, page, hookContext);
1251
1249
  await scenario.run(ctx);
1252
1250
  const durationMs = Date.now() - startTime;
1253
1251
  const snapshots = captureEngine.getSnapshots();
@@ -1262,38 +1260,19 @@ async function runVisualScenario(options2) {
1262
1260
  console.log(`
1263
1261
  \u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
1264
1262
  }
1265
- const reportPath = VisualReporter.generateReport({
1263
+ const reportData = {
1266
1264
  scenario,
1267
1265
  snapshots,
1268
1266
  consoleErrors,
1269
1267
  consoleWarnings,
1270
1268
  outputDir: scenarioArtifactsDir,
1271
1269
  targetMode,
1272
- durationMs
1273
- });
1274
- const syncLatest = (repPath, snaps) => {
1275
- try {
1276
- const latestDir = import_path6.default.join(artifactsRoot, "latest");
1277
- if (import_fs6.default.existsSync(latestDir)) {
1278
- import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
1279
- }
1280
- import_fs6.default.mkdirSync(latestDir, { recursive: true });
1281
- import_fs6.default.copyFileSync(repPath, import_path6.default.join(latestDir, "report.md"));
1282
- const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
1283
- if (import_fs6.default.existsSync(manifestSrc)) {
1284
- import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
1285
- }
1286
- for (const snap of snaps) {
1287
- if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
1288
- import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
1289
- }
1290
- }
1291
- return import_path6.default.join(latestDir, "report.md");
1292
- } catch {
1293
- return void 0;
1294
- }
1270
+ durationMs,
1271
+ customSections: []
1295
1272
  };
1296
- const latestReport = syncLatest(reportPath, snapshots);
1273
+ await pluginManager.runOnAfterRun(reportData, hookContext);
1274
+ const reportPath = VisualReporter.generateReport(reportData);
1275
+ const latestReport = syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1297
1276
  console.log(`
1298
1277
  \u2705 Visual Test Completed Successfully!`);
1299
1278
  console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
@@ -1320,7 +1299,7 @@ async function runVisualScenario(options2) {
1320
1299
  const snapshots = captureEngine.getSnapshots();
1321
1300
  const consoleErrors = consoleTracker.getErrors();
1322
1301
  const consoleWarnings = consoleTracker.getWarnings();
1323
- const reportPath = VisualReporter.generateReport({
1302
+ const reportData = {
1324
1303
  scenario,
1325
1304
  snapshots,
1326
1305
  consoleErrors,
@@ -1328,16 +1307,9 @@ async function runVisualScenario(options2) {
1328
1307
  outputDir: scenarioArtifactsDir,
1329
1308
  targetMode,
1330
1309
  durationMs
1331
- });
1332
- try {
1333
- const latestDir = import_path6.default.join(artifactsRoot, "latest");
1334
- if (import_fs6.default.existsSync(latestDir)) {
1335
- import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
1336
- }
1337
- import_fs6.default.mkdirSync(latestDir, { recursive: true });
1338
- import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
1339
- } catch {
1340
- }
1310
+ };
1311
+ const reportPath = VisualReporter.generateReport(reportData);
1312
+ syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1341
1313
  return {
1342
1314
  scenarioId: scenario.id,
1343
1315
  targetMode,
@@ -1350,6 +1322,7 @@ async function runVisualScenario(options2) {
1350
1322
  error: err.message
1351
1323
  };
1352
1324
  } finally {
1325
+ await pluginManager.runTeardown(hookContext);
1353
1326
  if (typeof scenario.teardown === "function") {
1354
1327
  try {
1355
1328
  console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
@@ -1358,21 +1331,21 @@ async function runVisualScenario(options2) {
1358
1331
  console.error(`\u26A0\uFE0F [Scenario Teardown] Error during teardown:`, teardownErr.message);
1359
1332
  }
1360
1333
  }
1361
- if (options2.cleanPaths && options2.cleanPaths.length > 0) {
1362
- for (const cleanTarget of options2.cleanPaths) {
1334
+ if (options.cleanPaths && options.cleanPaths.length > 0) {
1335
+ for (const cleanTarget of options.cleanPaths) {
1363
1336
  try {
1364
- const resolvedCleanPath = import_path6.default.resolve(process.cwd(), cleanTarget);
1365
- if (import_fs6.default.existsSync(resolvedCleanPath)) {
1337
+ const resolvedCleanPath = import_path7.default.resolve(process.cwd(), cleanTarget);
1338
+ if (import_fs7.default.existsSync(resolvedCleanPath)) {
1366
1339
  console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
1367
- import_fs6.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
1340
+ import_fs7.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
1368
1341
  }
1369
1342
  } catch (cleanErr) {
1370
1343
  console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
1371
1344
  }
1372
1345
  }
1373
1346
  }
1374
- if (!options2.detach) {
1375
- if (desktopDriver) await desktopDriver.stop();
1347
+ if (!options.detach) {
1348
+ if (customDriverStop) await customDriverStop();
1376
1349
  if (previewDriver) await previewDriver.stop();
1377
1350
  if (processManager) await processManager.stop();
1378
1351
  } else {
@@ -1381,12 +1354,9 @@ async function runVisualScenario(options2) {
1381
1354
  }
1382
1355
  }
1383
1356
 
1384
- // src/app/cli.ts
1385
- var import_jiti = require("jiti");
1386
-
1387
1357
  // src/features/snap/snap.ts
1388
- var import_path7 = __toESM(require("path"));
1389
- var import_fs7 = __toESM(require("fs"));
1358
+ var import_path8 = __toESM(require("path"));
1359
+ var import_fs8 = __toESM(require("fs"));
1390
1360
  var PRESET_MAP = {
1391
1361
  default: VIEWPORT_PRESETS.DEFAULT,
1392
1362
  desktop: VIEWPORT_PRESETS.DEFAULT,
@@ -1415,6 +1385,7 @@ function parseViewportPresets(raw) {
1415
1385
  }
1416
1386
  return presets.length > 0 ? presets : [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
1417
1387
  }
1388
+ var CANDIDATE_PORTS = [5173, 3e3, 4321, 4200, 8080, 8e3, 3001];
1418
1389
  async function isPortResponding(url) {
1419
1390
  try {
1420
1391
  const res = await fetch(url, { signal: AbortSignal.timeout(800) });
@@ -1423,26 +1394,33 @@ async function isPortResponding(url) {
1423
1394
  return false;
1424
1395
  }
1425
1396
  }
1426
- async function runQuickSnap(options2) {
1427
- let targetUrl = options2.url;
1397
+ async function detectActiveDevServer() {
1398
+ for (const port of CANDIDATE_PORTS) {
1399
+ const url = `http://localhost:${port}`;
1400
+ if (await isPortResponding(url)) {
1401
+ return url;
1402
+ }
1403
+ }
1404
+ return null;
1405
+ }
1406
+ async function runQuickSnap(options) {
1407
+ let targetUrl = options.url;
1428
1408
  let useStaticPreview = false;
1429
1409
  let wwwrootDir;
1430
- if (!targetUrl && !options2.start) {
1431
- if (await isPortResponding("http://localhost:5173")) {
1432
- targetUrl = "http://localhost:5173";
1433
- console.log(`\u{1F310} [Quick Snap] Detected active dev server on http://localhost:5173`);
1434
- } else if (await isPortResponding("http://localhost:3000")) {
1435
- targetUrl = "http://localhost:3000";
1436
- console.log(`\u{1F310} [Quick Snap] Detected active dev server on http://localhost:3000`);
1410
+ if (!targetUrl && !options.start) {
1411
+ const activeUrl = await detectActiveDevServer();
1412
+ if (activeUrl) {
1413
+ targetUrl = activeUrl;
1414
+ console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
1437
1415
  } else {
1438
1416
  const candidateDir = resolveWwwrootDir();
1439
- if (import_fs7.default.existsSync(candidateDir) && import_fs7.default.existsSync(import_path7.default.join(candidateDir, "index.html"))) {
1417
+ if (import_fs8.default.existsSync(candidateDir) && import_fs8.default.existsSync(import_path8.default.join(candidateDir, "index.html"))) {
1440
1418
  useStaticPreview = true;
1441
1419
  wwwrootDir = candidateDir;
1442
1420
  console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
1443
1421
  } else {
1444
1422
  console.error(`
1445
- \u274C [Quick Snap] No active server found on http://localhost:5173 or :3000, and no static build found.`);
1423
+ \u274C [Quick Snap] No active server found on common ports (${CANDIDATE_PORTS.join(", ")}), and no static build found.`);
1446
1424
  console.log(`\u{1F4A1} Suggested actions:`);
1447
1425
  console.log(` 1. Pass a start command: npx agent-lens snap --start="npm run dev"`);
1448
1426
  console.log(` 2. Specify your URL: npx agent-lens snap --url=http://localhost:8080`);
@@ -1451,17 +1429,17 @@ async function runQuickSnap(options2) {
1451
1429
  return false;
1452
1430
  }
1453
1431
  }
1454
- } else if (!targetUrl && options2.start) {
1432
+ } else if (!targetUrl && options.start) {
1455
1433
  targetUrl = "http://localhost:5173";
1456
1434
  }
1457
- const waitMs = options2.waitMs ?? 1e3;
1458
- const snapshotPrefix = options2.name || "quick_snap";
1459
- const targetViewports = parseViewportPresets(options2.viewports);
1435
+ const waitMs = options.waitMs ?? 1e3;
1436
+ const snapshotPrefix = options.name || "quick_snap";
1437
+ const targetViewports = parseViewportPresets(options.viewports);
1460
1438
  console.log(`
1461
1439
  \u{1F4F8} [Quick Snap] Preparing instant verification for: ${targetUrl || wwwrootDir}`);
1462
1440
  console.log(`\u{1F4D0} [Quick Snap] Testing ${targetViewports.length} viewports: ${targetViewports.map((v) => `${v.name} (${v.width}x${v.height})`).join(", ")}`);
1463
- if (options2.selector) {
1464
- console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${options2.selector}"`);
1441
+ if (options.selector) {
1442
+ console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${options.selector}"`);
1465
1443
  }
1466
1444
  const snapScenario = defineVisualTest({
1467
1445
  id: "quick-snap",
@@ -1478,16 +1456,16 @@ async function runQuickSnap(options2) {
1478
1456
  ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
1479
1457
  await ctx.setPreset(vp);
1480
1458
  await ctx.wait(200);
1481
- await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`);
1459
+ await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
1482
1460
  }
1483
- if (options2.selector) {
1484
- ctx.log(`Focusing on selector: "${options2.selector}"`);
1461
+ if (options.selector) {
1462
+ ctx.log(`Focusing on selector: "${options.selector}"`);
1485
1463
  try {
1486
1464
  await ctx.setPreset(VIEWPORT_PRESETS.DEFAULT);
1487
- await ctx.resizeToFit(options2.selector, 15);
1488
- await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector: options2.selector });
1465
+ await ctx.resizeToFit(options.selector, 15);
1466
+ await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector: options.selector });
1489
1467
  } catch (err) {
1490
- ctx.log(`\u26A0\uFE0F Could not isolate selector "${options2.selector}": ${err.message}`);
1468
+ ctx.log(`\u26A0\uFE0F Could not isolate selector "${options.selector}": ${err.message}`);
1491
1469
  }
1492
1470
  }
1493
1471
  const errors = ctx.getConsoleErrors();
@@ -1500,18 +1478,19 @@ async function runQuickSnap(options2) {
1500
1478
  });
1501
1479
  const result = await runVisualScenario({
1502
1480
  scenario: snapScenario,
1503
- targetMode: options2.mode || "preview",
1481
+ targetMode: options.mode || "preview",
1504
1482
  url: useStaticPreview ? void 0 : targetUrl,
1505
1483
  wwwrootDir: useStaticPreview ? wwwrootDir : void 0,
1506
- startCommand: options2.start,
1507
- startCwd: options2.startCwd,
1508
- executablePath: options2.exe,
1509
- cleanPaths: options2.clean,
1510
- cleanArtifacts: options2.cleanArtifacts,
1511
- port: options2.port,
1512
- headed: options2.headed,
1513
- detach: options2.detach,
1514
- artifactsRoot: options2.outDir ? import_path7.default.resolve(process.cwd(), options2.outDir) : void 0
1484
+ startCommand: options.start,
1485
+ startCwd: options.startCwd,
1486
+ executablePath: options.exe,
1487
+ cleanPaths: options.clean,
1488
+ cleanArtifacts: options.cleanArtifacts,
1489
+ port: options.port,
1490
+ headed: options.headed,
1491
+ detach: options.detach,
1492
+ artifactsRoot: options.outDir ? import_path8.default.resolve(process.cwd(), options.outDir) : void 0,
1493
+ plugins: options.plugins
1515
1494
  });
1516
1495
  console.log(`
1517
1496
  ========================================`);
@@ -1525,96 +1504,112 @@ async function runQuickSnap(options2) {
1525
1504
  return result.success && result.consoleErrors === 0;
1526
1505
  }
1527
1506
 
1528
- // src/app/cli.ts
1529
- var fileConfig = loadConfig();
1530
- var args = process.argv.slice(2);
1531
- if (args[0] === "init") {
1532
- initScenarioTemplate();
1533
- process.exit(0);
1534
- }
1535
- var isSnapCommand = args[0] === "snap";
1536
- var effectiveArgs = isSnapCommand ? args.slice(1) : args;
1537
- var options = {
1538
- mode: fileConfig.mode || "preview",
1539
- port: fileConfig.port || 9222,
1540
- headed: fileConfig.headed ?? false,
1541
- detach: fileConfig.detach ?? false,
1542
- build: fileConfig.buildCommand || false,
1543
- start: fileConfig.startCommand,
1544
- startCwd: fileConfig.startCwd,
1545
- cleanArtifacts: fileConfig.cleanArtifacts ?? false,
1546
- url: fileConfig.url,
1547
- exe: fileConfig.executablePath,
1548
- clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
1549
- dir: fileConfig.scenarios,
1550
- wwwroot: fileConfig.wwwroot,
1551
- outDir: fileConfig.outDir
1552
- };
1553
- for (const arg of effectiveArgs) {
1554
- if (arg === "--help" || arg === "-h") {
1555
- options.help = true;
1556
- } else if (arg.startsWith("--scenario=")) {
1557
- options.scenario = arg.split("=")[1];
1558
- } else if (arg === "--all") {
1559
- options.all = true;
1560
- } else if (arg.startsWith("--mode=")) {
1561
- const mode = arg.split("=")[1].toLowerCase();
1562
- if (mode === "desktop" || mode === "preview") {
1563
- options.mode = mode;
1564
- }
1565
- } else if (arg.startsWith("--port=")) {
1566
- options.port = parseInt(arg.split("=")[1], 10) || 9222;
1567
- } else if (arg === "--headed") {
1568
- options.headed = true;
1569
- } else if (arg === "--detach") {
1570
- options.detach = true;
1571
- } else if (arg === "--build") {
1572
- options.build = true;
1573
- } else if (arg.startsWith("--build=")) {
1574
- options.build = arg.slice("--build=".length);
1575
- } else if (arg.startsWith("--start=")) {
1576
- options.start = arg.slice("--start=".length);
1577
- } else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
1578
- options.startCwd = arg.split("=")[1];
1579
- } else if (arg === "--clean-artifacts") {
1580
- options.cleanArtifacts = true;
1581
- } else if (arg.startsWith("--url=")) {
1582
- options.url = arg.split("=")[1];
1583
- } else if (arg.startsWith("--selector=")) {
1584
- options.selector = arg.split("=")[1];
1585
- } else if (arg.startsWith("--viewports=")) {
1586
- options.viewports = arg.split("=")[1].split(",").map((v) => v.trim()).filter(Boolean);
1587
- } else if (arg.startsWith("--wait=")) {
1588
- options.waitMs = parseInt(arg.split("=")[1], 10);
1589
- } else if (arg.startsWith("--name=")) {
1590
- options.name = arg.split("=")[1];
1591
- } else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
1592
- options.exe = arg.split("=")[1];
1593
- } else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
1594
- const rawPaths = arg.split("=")[1];
1595
- options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
1596
- } else if (arg.startsWith("--dir=")) {
1597
- options.dir = arg.split("=")[1];
1598
- } else if (arg.startsWith("--wwwroot=")) {
1599
- options.wwwroot = arg.split("=")[1];
1600
- } else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
1601
- options.outDir = arg.split("=")[1];
1507
+ // src/app/lib/argsParser.ts
1508
+ function parseCliArgs(argv, fileConfig) {
1509
+ const isInitCommand = argv[0] === "init";
1510
+ const isSnapCommand = argv[0] === "snap";
1511
+ const effectiveArgs = isSnapCommand ? argv.slice(1) : argv;
1512
+ const options = {
1513
+ mode: fileConfig.mode || "preview",
1514
+ port: fileConfig.port || 9222,
1515
+ headed: fileConfig.headed ?? false,
1516
+ detach: fileConfig.detach ?? false,
1517
+ build: fileConfig.buildCommand || false,
1518
+ start: fileConfig.startCommand,
1519
+ startCwd: fileConfig.startCwd,
1520
+ cleanArtifacts: fileConfig.cleanArtifacts ?? false,
1521
+ url: fileConfig.url,
1522
+ exe: fileConfig.executablePath,
1523
+ clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
1524
+ dir: fileConfig.scenarios,
1525
+ wwwroot: fileConfig.wwwroot,
1526
+ outDir: fileConfig.outDir,
1527
+ plugins: fileConfig.plugins ? [...fileConfig.plugins] : []
1528
+ };
1529
+ for (const arg of effectiveArgs) {
1530
+ if (arg === "--help" || arg === "-h") {
1531
+ options.help = true;
1532
+ } else if (arg.startsWith("--scenario=")) {
1533
+ options.scenario = arg.split("=")[1];
1534
+ } else if (arg === "--all") {
1535
+ options.all = true;
1536
+ } else if (arg.startsWith("--mode=")) {
1537
+ options.mode = arg.split("=")[1].toLowerCase();
1538
+ } else if (arg.startsWith("--port=")) {
1539
+ options.port = parseInt(arg.split("=")[1], 10) || 9222;
1540
+ } else if (arg === "--headed") {
1541
+ options.headed = true;
1542
+ } else if (arg === "--detach") {
1543
+ options.detach = true;
1544
+ } else if (arg === "--build") {
1545
+ options.build = true;
1546
+ } else if (arg.startsWith("--build=")) {
1547
+ options.build = arg.slice("--build=".length);
1548
+ } else if (arg.startsWith("--start=")) {
1549
+ options.start = arg.slice("--start=".length);
1550
+ } else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
1551
+ options.startCwd = arg.split("=")[1];
1552
+ } else if (arg === "--clean-artifacts") {
1553
+ options.cleanArtifacts = true;
1554
+ } else if (arg.startsWith("--url=")) {
1555
+ options.url = arg.split("=")[1];
1556
+ } else if (arg.startsWith("--selector=")) {
1557
+ options.selector = arg.split("=")[1];
1558
+ } else if (arg.startsWith("--viewports=")) {
1559
+ const raw = arg.slice("--viewports=".length);
1560
+ options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
1561
+ } else if (arg.startsWith("--wait=")) {
1562
+ options.waitMs = parseInt(arg.slice("--wait=".length), 10);
1563
+ } else if (arg.startsWith("--name=")) {
1564
+ options.name = arg.slice("--name=".length);
1565
+ } else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
1566
+ const prefix = arg.startsWith("--exe=") ? "--exe=" : "--executable=";
1567
+ options.exe = arg.slice(prefix.length);
1568
+ } else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
1569
+ const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
1570
+ const rawPaths = arg.slice(prefix.length);
1571
+ options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
1572
+ } else if (arg.startsWith("--dir=")) {
1573
+ options.dir = arg.split("=")[1];
1574
+ } else if (arg.startsWith("--wwwroot=")) {
1575
+ options.wwwroot = arg.split("=")[1];
1576
+ } else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
1577
+ options.outDir = arg.split("=")[1];
1578
+ } else if (arg.startsWith("--plugin=") || arg.startsWith("--plugins=")) {
1579
+ const raw = arg.split("=")[1] || "";
1580
+ const items = raw.split(",").map((p) => p.trim()).filter(Boolean);
1581
+ options.plugins = [...options.plugins || [], ...items];
1582
+ } else if (arg === "--full" || arg === "--full-page") {
1583
+ options.fullPage = true;
1584
+ }
1602
1585
  }
1586
+ if (options.start && !options.startCwd) {
1587
+ options.startCwd = detectStartCwd(options.startCwd);
1588
+ }
1589
+ if ((options.mode === "desktop" || options.exe) && !options.plugins?.includes("desktop-webview2")) {
1590
+ options.plugins = ["desktop-webview2", ...options.plugins || []];
1591
+ }
1592
+ return {
1593
+ isSnapCommand,
1594
+ isInitCommand,
1595
+ options
1596
+ };
1603
1597
  }
1604
- if (options.start && !options.startCwd) {
1605
- options.startCwd = detectStartCwd(options.startCwd);
1606
- }
1598
+
1599
+ // src/app/lib/help.ts
1607
1600
  function printHelp() {
1608
1601
  console.log(`
1609
1602
  \u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
1610
1603
 
1611
1604
  Usage:
1612
1605
  npx agent-lens snap [options] Instant one-shot visual & console check (no test files needed)
1606
+ npx agent-lens live [action] Interactive live session (start, click, type, snap, stop)
1613
1607
  npx agent-lens [options] Run scripted scenario tests from scenarios/
1614
1608
  npx agent-lens init Generate starter scenario template & mocks
1615
1609
 
1616
1610
  Commands:
1617
1611
  snap Take immediate multi-viewport screenshots of a URL & check console errors
1612
+ live Interactive control: click coordinates/selectors, type text, snap --full
1618
1613
  init Generate starter template in scenarios/template.scenario.ts and mocks.ts
1619
1614
 
1620
1615
  Options:
@@ -1622,6 +1617,7 @@ Options:
1622
1617
  --start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
1623
1618
  --start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
1624
1619
  --clean-artifacts Purge previous test artifacts to prevent folder bloat
1620
+ --full Capture full scrollable page instead of only the viewport
1625
1621
  --selector=<css> Target a specific element to focus on / resize-to-fit
1626
1622
  --viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
1627
1623
  --wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
@@ -1633,6 +1629,7 @@ Options:
1633
1629
  --port=<port> CDP remote debugging port [default: 9222]
1634
1630
  --build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
1635
1631
  --clean=<paths> Comma-separated paths to safely delete upon test exit (e.g. --clean="./temp,./cache")
1632
+ --plugin=<names> Comma-separated plugins (e.g. --plugin=desktop-webview2,mock-ipc)
1636
1633
  --headed Show Chromium browser window
1637
1634
  --detach Keep browser/app open after finishing
1638
1635
  --dir=<path> Custom scenarios directory [default: scenarios]
@@ -1641,13 +1638,17 @@ Options:
1641
1638
  --help, -h Show this help message
1642
1639
  `);
1643
1640
  }
1641
+
1642
+ // src/app/lib/templateInit.ts
1643
+ var import_path9 = __toESM(require("path"));
1644
+ var import_fs9 = __toESM(require("fs"));
1644
1645
  function initScenarioTemplate() {
1645
- const targetDir = import_path8.default.resolve(process.cwd(), "scenarios");
1646
- if (!import_fs8.default.existsSync(targetDir)) {
1647
- import_fs8.default.mkdirSync(targetDir, { recursive: true });
1646
+ const targetDir = import_path9.default.resolve(process.cwd(), "scenarios");
1647
+ if (!import_fs9.default.existsSync(targetDir)) {
1648
+ import_fs9.default.mkdirSync(targetDir, { recursive: true });
1648
1649
  }
1649
- const templatePath = import_path8.default.join(targetDir, "template.scenario.ts");
1650
- if (!import_fs8.default.existsSync(templatePath)) {
1650
+ const templatePath = import_path9.default.join(targetDir, "template.scenario.ts");
1651
+ if (!import_fs9.default.existsSync(templatePath)) {
1651
1652
  const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
1652
1653
 
1653
1654
  export default defineVisualTest({
@@ -1678,13 +1679,13 @@ export default defineVisualTest({
1678
1679
  }
1679
1680
  });
1680
1681
  `;
1681
- import_fs8.default.writeFileSync(templatePath, templateContent, "utf8");
1682
+ import_fs9.default.writeFileSync(templatePath, templateContent, "utf8");
1682
1683
  console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
1683
1684
  } else {
1684
1685
  console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
1685
1686
  }
1686
- const mocksPath = import_path8.default.join(targetDir, "mocks.ts");
1687
- if (!import_fs8.default.existsSync(mocksPath)) {
1687
+ const mocksPath = import_path9.default.join(targetDir, "mocks.ts");
1688
+ if (!import_fs9.default.existsSync(mocksPath)) {
1688
1689
  const mocksContent = `/**
1689
1690
  * Global IPC & API Mocks
1690
1691
  *
@@ -1695,21 +1696,21 @@ export default [
1695
1696
  { action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
1696
1697
  ];
1697
1698
  `;
1698
- import_fs8.default.writeFileSync(mocksPath, mocksContent, "utf8");
1699
+ import_fs9.default.writeFileSync(mocksPath, mocksContent, "utf8");
1699
1700
  console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
1700
1701
  }
1701
1702
  console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
1702
1703
  }
1703
- if (options.help) {
1704
- printHelp();
1705
- process.exit(0);
1706
- }
1704
+
1705
+ // src/app/lib/scenarioFinder.ts
1706
+ var import_path10 = __toESM(require("path"));
1707
+ var import_fs10 = __toESM(require("fs"));
1707
1708
  async function findScenarios(dir, specificName) {
1708
1709
  const results = [];
1709
- if (!import_fs8.default.existsSync(dir)) return results;
1710
- const items = import_fs8.default.readdirSync(dir, { withFileTypes: true });
1710
+ if (!import_fs10.default.existsSync(dir)) return results;
1711
+ const items = import_fs10.default.readdirSync(dir, { withFileTypes: true });
1711
1712
  for (const item of items) {
1712
- const fullPath = import_path8.default.join(dir, item.name);
1713
+ const fullPath = import_path10.default.join(dir, item.name);
1713
1714
  if (item.isDirectory()) {
1714
1715
  results.push(...await findScenarios(fullPath, specificName));
1715
1716
  } else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
@@ -1722,7 +1723,7 @@ async function findScenarios(dir, specificName) {
1722
1723
  }
1723
1724
  function resolveScenariosDirectory(customDir) {
1724
1725
  if (customDir) {
1725
- return import_path8.default.resolve(process.cwd(), customDir);
1726
+ return import_path10.default.resolve(process.cwd(), customDir);
1726
1727
  }
1727
1728
  const candidates = [
1728
1729
  "scenarios",
@@ -1732,122 +1733,333 @@ function resolveScenariosDirectory(customDir) {
1732
1733
  "src/scenarios"
1733
1734
  ];
1734
1735
  for (const c of candidates) {
1735
- const p = import_path8.default.resolve(process.cwd(), c);
1736
- if (import_fs8.default.existsSync(p)) {
1736
+ const p = import_path10.default.resolve(process.cwd(), c);
1737
+ if (import_fs10.default.existsSync(p)) {
1737
1738
  return p;
1738
1739
  }
1739
1740
  }
1740
- return import_path8.default.resolve(process.cwd(), "scenarios");
1741
+ return import_path10.default.resolve(process.cwd(), "scenarios");
1742
+ }
1743
+
1744
+ // src/plugins/live-controller/actions.ts
1745
+ var import_path12 = __toESM(require("path"));
1746
+
1747
+ // src/plugins/live-controller/session.ts
1748
+ var import_fs11 = __toESM(require("fs"));
1749
+ var import_path11 = __toESM(require("path"));
1750
+ var import_tree_kill2 = __toESM(require("tree-kill"));
1751
+ var SESSION_DIR = import_path11.default.resolve(process.cwd(), ".agent-lens");
1752
+ var SESSION_FILE = import_path11.default.join(SESSION_DIR, "live-session.json");
1753
+ var LIVE_ARTIFACTS_DIR = import_path11.default.resolve(process.cwd(), "artifacts", "live");
1754
+ function getLiveArtifactsDir() {
1755
+ if (!import_fs11.default.existsSync(LIVE_ARTIFACTS_DIR)) {
1756
+ import_fs11.default.mkdirSync(LIVE_ARTIFACTS_DIR, { recursive: true });
1757
+ }
1758
+ return LIVE_ARTIFACTS_DIR;
1759
+ }
1760
+ function readLiveSession() {
1761
+ if (!import_fs11.default.existsSync(SESSION_FILE)) return null;
1762
+ try {
1763
+ return JSON.parse(import_fs11.default.readFileSync(SESSION_FILE, "utf-8"));
1764
+ } catch {
1765
+ return null;
1766
+ }
1767
+ }
1768
+ function saveLiveSession(info) {
1769
+ if (!import_fs11.default.existsSync(SESSION_DIR)) {
1770
+ import_fs11.default.mkdirSync(SESSION_DIR, { recursive: true });
1771
+ }
1772
+ import_fs11.default.writeFileSync(SESSION_FILE, JSON.stringify(info, null, 2), "utf-8");
1773
+ }
1774
+ function clearLiveSession() {
1775
+ if (import_fs11.default.existsSync(SESSION_FILE)) {
1776
+ try {
1777
+ import_fs11.default.unlinkSync(SESSION_FILE);
1778
+ } catch {
1779
+ }
1780
+ }
1781
+ }
1782
+ async function connectToLiveSession() {
1783
+ const session = readLiveSession();
1784
+ if (!session) {
1785
+ throw new Error(
1786
+ `No active live session found. Start one with: npx agent-lens live start --url=http://localhost:5173`
1787
+ );
1788
+ }
1789
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${session.port}`);
1790
+ const contexts = browser.contexts();
1791
+ const context = contexts[0] || await browser.newContext();
1792
+ const pages = context.pages();
1793
+ const page = pages[0] || await context.waitForEvent("page", { timeout: 5e3 });
1794
+ return { browser, context, page };
1741
1795
  }
1742
- async function main() {
1743
- if (isSnapCommand || options.url && !options.scenario && !options.all) {
1744
- const snapSuccess = await runQuickSnap({
1745
- url: options.url,
1746
- start: options.start,
1747
- startCwd: options.startCwd,
1748
- selector: options.selector,
1749
- viewports: options.viewports,
1750
- waitMs: options.waitMs,
1751
- name: options.name,
1752
- clean: options.clean,
1753
- cleanArtifacts: options.cleanArtifacts,
1754
- headed: options.headed,
1755
- detach: options.detach,
1756
- outDir: options.outDir,
1757
- mode: options.mode,
1758
- exe: options.exe,
1759
- port: options.port
1796
+ async function stopLiveSession() {
1797
+ const session = readLiveSession();
1798
+ if (!session) {
1799
+ console.log(`\u2139\uFE0F [Live] No active session to stop.`);
1800
+ return false;
1801
+ }
1802
+ try {
1803
+ const { browser } = await connectToLiveSession();
1804
+ await browser.close().catch(() => {
1760
1805
  });
1761
- process.exit(snapSuccess ? 0 : 1);
1806
+ } catch {
1762
1807
  }
1763
- if (!options.scenario && !options.all) {
1764
- console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
1765
- printHelp();
1766
- process.exit(1);
1808
+ if (session.pid) {
1809
+ await new Promise((resolve) => {
1810
+ (0, import_tree_kill2.default)(session.pid, "SIGTERM", () => resolve());
1811
+ });
1767
1812
  }
1768
- const scenariosDir = resolveScenariosDirectory(options.dir);
1769
- const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
1770
- if (scenarioPaths.length === 0) {
1771
- console.error(`\u274C No scenarios found in ${scenariosDir}`);
1772
- console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
1773
- process.exit(1);
1813
+ clearLiveSession();
1814
+ console.log(`\u{1F6D1} [Live] Session stopped and cleaned up.`);
1815
+ return true;
1816
+ }
1817
+
1818
+ // src/plugins/live-controller/actions.ts
1819
+ async function clickCoords(page, x, y, options) {
1820
+ console.log(`\u{1F5B1}\uFE0F [LiveController] Clicking coordinates: (${x}, ${y})`);
1821
+ await page.mouse.click(x, y, options);
1822
+ await page.waitForTimeout(200);
1823
+ }
1824
+ async function snapLive(page, options) {
1825
+ const artifactsDir = getLiveArtifactsDir();
1826
+ const fileName = options?.name ? `${options.name}.png` : "current.png";
1827
+ const filePath = import_path12.default.join(artifactsDir, fileName);
1828
+ if (options?.selector) {
1829
+ const el = await page.waitForSelector(options.selector, { timeout: 5e3 });
1830
+ await el.screenshot({ path: filePath });
1831
+ } else {
1832
+ await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
1774
1833
  }
1775
- const shouldBuild = options.build || fileConfig.autoBuild;
1776
- if (shouldBuild) {
1777
- const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
1778
- console.log(`
1779
- \u{1F528} [Build] Running build process: "${buildCmd}"...`);
1780
- const { execSync } = require("child_process");
1781
- try {
1782
- execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
1783
- } catch (e) {
1784
- console.error(`\u274C Build failed: ${buildCmd}`);
1785
- process.exit(1);
1834
+ console.log(`\u{1F4F8} [LiveController] Snapshot saved: ${filePath}${options?.fullPage ? " (Full Page)" : ""}`);
1835
+ return filePath;
1836
+ }
1837
+ async function handleLiveCli(argv) {
1838
+ const action = argv[0]?.toLowerCase();
1839
+ switch (action) {
1840
+ case "start": {
1841
+ const urlArg = argv.find((a) => a.startsWith("--url="))?.split("=")[1] || "http://localhost:5173";
1842
+ const portArg = parseInt(argv.find((a) => a.startsWith("--port="))?.split("=")[1] || "9223", 10);
1843
+ const isHeaded = argv.includes("--headed");
1844
+ console.log(`\u{1F680} [Live] Starting background browser for ${urlArg} on CDP port ${portArg}...`);
1845
+ const browser = await chromium.launch({
1846
+ headless: !isHeaded,
1847
+ args: [`--remote-debugging-port=${portArg}`, "--no-sandbox"]
1848
+ });
1849
+ const context = await browser.newContext({ viewport: { width: 1200, height: 800 } });
1850
+ const page = await context.newPage();
1851
+ await page.goto(urlArg, { waitUntil: "domcontentloaded" });
1852
+ await page.waitForTimeout(500);
1853
+ saveLiveSession({
1854
+ port: portArg,
1855
+ url: urlArg,
1856
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1857
+ });
1858
+ await snapLive(page, { name: "current" });
1859
+ console.log(`\u2705 [Live] Session active! You can now send live commands:
1860
+ `);
1861
+ console.log(` npx agent-lens live click 450 120`);
1862
+ console.log(` npx agent-lens live type "input" "hello"`);
1863
+ console.log(` npx agent-lens live snap --full`);
1864
+ console.log(` npx agent-lens live stop
1865
+ `);
1866
+ return true;
1786
1867
  }
1787
- } else if (options.mode === "preview" && !options.url && !options.start) {
1788
- console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
1789
- }
1790
- console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
1791
- const jiti = (0, import_jiti.createJiti)(process.cwd());
1792
- let globalMocks = [];
1793
- const mockCandidates = [
1794
- import_path8.default.join(scenariosDir, "mocks.ts"),
1795
- import_path8.default.join(scenariosDir, "mocks.js"),
1796
- import_path8.default.join(process.cwd(), "mocks.ts"),
1797
- import_path8.default.join(process.cwd(), "mocks.js")
1798
- ];
1799
- for (const mocksPath of mockCandidates) {
1800
- if (import_fs8.default.existsSync(mocksPath)) {
1801
- try {
1802
- const m = await jiti.import(mocksPath);
1803
- globalMocks = m.default || m.mocks || [];
1804
- console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path8.default.basename(mocksPath)}`);
1805
- break;
1806
- } catch (err) {
1807
- console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
1868
+ case "click": {
1869
+ const { page, browser } = await connectToLiveSession();
1870
+ const firstArg = argv[1];
1871
+ const secondArg = argv[2];
1872
+ const x = parseInt(firstArg, 10);
1873
+ const y = parseInt(secondArg, 10);
1874
+ if (!isNaN(x) && !isNaN(y)) {
1875
+ await clickCoords(page, x, y);
1876
+ } else if (firstArg) {
1877
+ console.log(`\u{1F5B1}\uFE0F [Live] Clicking selector: "${firstArg}"`);
1878
+ await page.click(firstArg);
1879
+ } else {
1880
+ console.error(`\u274C Usage: npx agent-lens live click <x> <y> OR npx agent-lens live click <selector>`);
1881
+ await browser.close();
1882
+ return false;
1808
1883
  }
1884
+ await snapLive(page, { name: "current" });
1885
+ return true;
1809
1886
  }
1810
- }
1811
- let allSuccess = true;
1812
- for (const scenarioPath of scenarioPaths) {
1813
- try {
1814
- const scenarioModule = await jiti.import(scenarioPath);
1815
- const scenario = scenarioModule.default || scenarioModule.scenario;
1816
- if (!scenario || typeof scenario.run !== "function") {
1817
- console.error(`\u26A0\uFE0F Skipped: file ${import_path8.default.basename(scenarioPath)} does not export a VisualScenario object by default.`);
1818
- continue;
1887
+ case "type": {
1888
+ const { page, browser } = await connectToLiveSession();
1889
+ const selector = argv[1];
1890
+ const text = argv[2];
1891
+ if (!selector || text === void 0) {
1892
+ console.error(`\u274C Usage: npx agent-lens live type <selector> <text>`);
1893
+ await browser.close();
1894
+ return false;
1819
1895
  }
1820
- const result = await runVisualScenario({
1821
- scenario,
1822
- targetMode: options.mode,
1896
+ console.log(`\u2328\uFE0F [Live] Typing into "${selector}": "${text}"`);
1897
+ await page.fill(selector, text);
1898
+ await snapLive(page, { name: "current" });
1899
+ return true;
1900
+ }
1901
+ case "snap": {
1902
+ const { page } = await connectToLiveSession();
1903
+ const isFull = argv.includes("--full");
1904
+ const nameArg = argv.find((a) => !a.startsWith("--") && a !== "snap");
1905
+ await snapLive(page, { name: nameArg || "current", fullPage: isFull });
1906
+ return true;
1907
+ }
1908
+ case "stop": {
1909
+ return await stopLiveSession();
1910
+ }
1911
+ default: {
1912
+ console.log(`
1913
+ \u2139\uFE0F AgentLens Live Controller:
1914
+ npx agent-lens live start --url=<url> Start background live session
1915
+ npx agent-lens live click <x> <y> Click at pixel coordinates
1916
+ npx agent-lens live click <selector> Click CSS selector
1917
+ npx agent-lens live type <sel> <text> Fill input field
1918
+ npx agent-lens live snap [name] [--full] Capture current or full-page screenshot
1919
+ npx agent-lens live stop Close live browser and finish
1920
+ `);
1921
+ return true;
1922
+ }
1923
+ }
1924
+ }
1925
+
1926
+ // src/app/cli.ts
1927
+ var rawArgs = process.argv.slice(2);
1928
+ if (rawArgs[0] === "live") {
1929
+ handleLiveCli(rawArgs.slice(1)).then((success) => process.exit(success ? 0 : 1)).catch((err) => {
1930
+ console.error("\u274C Live command failed:", err instanceof Error ? err.message : err);
1931
+ process.exit(1);
1932
+ });
1933
+ } else {
1934
+ const fileConfig = loadConfig();
1935
+ const { isSnapCommand, isInitCommand, options } = parseCliArgs(rawArgs, fileConfig);
1936
+ if (isInitCommand) {
1937
+ initScenarioTemplate();
1938
+ process.exit(0);
1939
+ }
1940
+ if (options.help) {
1941
+ printHelp();
1942
+ process.exit(0);
1943
+ }
1944
+ async function main() {
1945
+ if (isSnapCommand || options.url && !options.scenario && !options.all) {
1946
+ const snapSuccess = await runQuickSnap({
1823
1947
  url: options.url,
1824
- startCommand: options.start,
1948
+ start: options.start,
1825
1949
  startCwd: options.startCwd,
1950
+ selector: options.selector,
1951
+ viewports: options.viewports,
1952
+ waitMs: options.waitMs,
1953
+ name: options.name,
1954
+ clean: options.clean,
1826
1955
  cleanArtifacts: options.cleanArtifacts,
1827
- port: options.port,
1828
1956
  headed: options.headed,
1829
1957
  detach: options.detach,
1830
- executablePath: options.exe,
1831
- cleanPaths: options.clean,
1832
- desktopEnv: fileConfig.env,
1833
- wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
1834
- artifactsRoot: options.outDir ? import_path8.default.resolve(process.cwd(), options.outDir) : void 0,
1835
- globalMocks
1958
+ outDir: options.outDir,
1959
+ mode: options.mode,
1960
+ exe: options.exe,
1961
+ port: options.port,
1962
+ plugins: options.plugins,
1963
+ fullPage: options.fullPage
1836
1964
  });
1837
- if (!result.success) {
1965
+ process.exit(snapSuccess ? 0 : 1);
1966
+ }
1967
+ if (!options.scenario && !options.all) {
1968
+ console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
1969
+ printHelp();
1970
+ process.exit(1);
1971
+ }
1972
+ const scenariosDir = resolveScenariosDirectory(options.dir);
1973
+ const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
1974
+ if (scenarioPaths.length === 0) {
1975
+ console.error(`\u274C No scenarios found in ${scenariosDir}`);
1976
+ console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
1977
+ process.exit(1);
1978
+ }
1979
+ const shouldBuild = options.build || fileConfig.autoBuild;
1980
+ if (shouldBuild) {
1981
+ const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
1982
+ console.log(`
1983
+ \u{1F528} [Build] Running build process: "${buildCmd}"...`);
1984
+ try {
1985
+ (0, import_child_process2.execSync)(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
1986
+ } catch (e) {
1987
+ console.error(`\u274C Build failed: ${buildCmd}`);
1988
+ process.exit(1);
1989
+ }
1990
+ } else if (options.mode === "preview" && !options.url && !options.start) {
1991
+ console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
1992
+ }
1993
+ console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
1994
+ const jiti = (0, import_jiti2.createJiti)(process.cwd());
1995
+ let globalMocks = [];
1996
+ const mockCandidates = [
1997
+ import_path13.default.join(scenariosDir, "mocks.ts"),
1998
+ import_path13.default.join(scenariosDir, "mocks.js"),
1999
+ import_path13.default.join(process.cwd(), "mocks.ts"),
2000
+ import_path13.default.join(process.cwd(), "mocks.js")
2001
+ ];
2002
+ for (const mocksPath of mockCandidates) {
2003
+ if (import_fs12.default.existsSync(mocksPath)) {
2004
+ try {
2005
+ const m = await jiti.import(mocksPath);
2006
+ globalMocks = m.default || m.mocks || [];
2007
+ console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path13.default.basename(mocksPath)}`);
2008
+ break;
2009
+ } catch (err) {
2010
+ console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
2011
+ }
2012
+ }
2013
+ }
2014
+ if (globalMocks.length > 0 && !options.plugins?.includes("mock-ipc")) {
2015
+ options.plugins = ["mock-ipc", ...options.plugins || []];
2016
+ }
2017
+ let allSuccess = true;
2018
+ for (const scenarioPath of scenarioPaths) {
2019
+ try {
2020
+ const scenarioModule = await jiti.import(scenarioPath);
2021
+ const scenario = scenarioModule.default || scenarioModule.scenario;
2022
+ if (!scenario || typeof scenario.run !== "function") {
2023
+ console.error(`\u26A0\uFE0F Skipped: file ${import_path13.default.basename(scenarioPath)} does not export a VisualScenario object by default.`);
2024
+ continue;
2025
+ }
2026
+ const scenarioPlugins = [...options.plugins || []];
2027
+ if (scenario.mockIpc && scenario.mockIpc.length > 0 && !scenarioPlugins.includes("mock-ipc")) {
2028
+ scenarioPlugins.push("mock-ipc");
2029
+ }
2030
+ const result = await runVisualScenario({
2031
+ scenario,
2032
+ targetMode: options.mode,
2033
+ url: options.url,
2034
+ startCommand: options.start,
2035
+ startCwd: options.startCwd,
2036
+ cleanArtifacts: options.cleanArtifacts,
2037
+ port: options.port,
2038
+ headed: options.headed,
2039
+ detach: options.detach,
2040
+ executablePath: options.exe,
2041
+ cleanPaths: options.clean,
2042
+ desktopEnv: fileConfig.env,
2043
+ wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
2044
+ artifactsRoot: options.outDir ? import_path13.default.resolve(process.cwd(), options.outDir) : void 0,
2045
+ globalMocks,
2046
+ plugins: scenarioPlugins
2047
+ });
2048
+ if (!result.success) {
2049
+ allSuccess = false;
2050
+ }
2051
+ } catch (err) {
2052
+ console.error(`\u274C Failed to load scenario ${import_path13.default.basename(scenarioPath)}:`, err);
1838
2053
  allSuccess = false;
1839
2054
  }
1840
- } catch (err) {
1841
- console.error(`\u274C Failed to load scenario ${import_path8.default.basename(scenarioPath)}:`, err);
1842
- allSuccess = false;
2055
+ }
2056
+ if (!allSuccess) {
2057
+ process.exit(1);
1843
2058
  }
1844
2059
  }
1845
- if (!allSuccess) {
2060
+ main().catch((err) => {
2061
+ console.error("Fatal error:", err);
1846
2062
  process.exit(1);
1847
- }
2063
+ });
1848
2064
  }
1849
- main().catch((err) => {
1850
- console.error("Fatal error:", err);
1851
- process.exit(1);
1852
- });
1853
2065
  //# sourceMappingURL=cli.js.map