@_deep4wee/agent-lens 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +331 -227
  2. package/dist/cli.js +1068 -911
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.mjs +1277 -1033
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/dsl-BIjVN1M0.d.mts +204 -0
  7. package/dist/dsl-BIjVN1M0.d.ts +204 -0
  8. package/dist/index.d.mts +131 -178
  9. package/dist/index.d.ts +131 -178
  10. package/dist/index.js +1506 -8
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1479 -3
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/plugins/a11y-tree/index.d.mts +11 -0
  15. package/dist/plugins/a11y-tree/index.d.ts +11 -0
  16. package/dist/plugins/a11y-tree/index.js +190 -0
  17. package/dist/plugins/a11y-tree/index.js.map +1 -0
  18. package/dist/plugins/a11y-tree/index.mjs +155 -0
  19. package/dist/plugins/a11y-tree/index.mjs.map +1 -0
  20. package/dist/plugins/desktop-webview2/index.d.mts +14 -0
  21. package/dist/plugins/desktop-webview2/index.d.ts +14 -0
  22. package/dist/plugins/desktop-webview2/index.js +258 -0
  23. package/dist/plugins/desktop-webview2/index.js.map +1 -0
  24. package/dist/plugins/desktop-webview2/index.mjs +221 -0
  25. package/dist/plugins/desktop-webview2/index.mjs.map +1 -0
  26. package/dist/plugins/live-controller/index.d.mts +35 -0
  27. package/dist/plugins/live-controller/index.d.ts +35 -0
  28. package/dist/plugins/live-controller/index.js +303 -0
  29. package/dist/plugins/live-controller/index.js.map +1 -0
  30. package/dist/plugins/live-controller/index.mjs +261 -0
  31. package/dist/plugins/live-controller/index.mjs.map +1 -0
  32. package/dist/plugins/mock-ipc/index.d.mts +30 -0
  33. package/dist/plugins/mock-ipc/index.d.ts +30 -0
  34. package/dist/plugins/mock-ipc/index.js +210 -0
  35. package/dist/plugins/mock-ipc/index.js.map +1 -0
  36. package/dist/plugins/mock-ipc/index.mjs +181 -0
  37. package/dist/plugins/mock-ipc/index.mjs.map +1 -0
  38. package/dist/plugins/visual-diff/index.d.mts +30 -0
  39. package/dist/plugins/visual-diff/index.d.ts +30 -0
  40. package/dist/plugins/visual-diff/index.js +163 -0
  41. package/dist/plugins/visual-diff/index.js.map +1 -0
  42. package/dist/plugins/visual-diff/index.mjs +127 -0
  43. package/dist/plugins/visual-diff/index.mjs.map +1 -0
  44. package/docs/plugins.md +415 -0
  45. package/package.json +40 -2
  46. package/skills/agent-lens/SKILL.md +130 -43
  47. package/skills/agent-lens/examples/07-live-controller-interactive-loop.md +108 -0
  48. package/skills/agent-lens/examples/08-accessibility-semantic-inspection.md +80 -0
  49. package/skills/agent-lens/examples/09-visual-regression-and-pixel-diffing.md +66 -0
  50. package/skills/agent-lens/examples/10-authoring-custom-agent-plugins.md +85 -0
  51. package/skills/agent-lens/references/plugin-development.md +165 -0
package/dist/cli.js CHANGED
@@ -23,19 +23,102 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/app/cli.ts
26
- var import_path8 = __toESM(require("path"));
27
- var import_fs8 = __toESM(require("fs"));
28
- var import_child_process3 = require("child_process");
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();
@@ -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,6 +381,7 @@ ${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:
@@ -300,7 +395,7 @@ ${consoleSections}
300
395
  - [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
301
396
 
302
397
  `;
303
- import_fs2.default.writeFileSync(reportPath, reportContent, "utf-8");
398
+ import_fs3.default.writeFileSync(reportPath, reportContent, "utf-8");
304
399
  return reportPath;
305
400
  }
306
401
  static getHealthStatus(errors, warnings) {
@@ -402,20 +497,19 @@ var ConsoleTracker = class {
402
497
  }
403
498
  };
404
499
 
405
- // src/shared/drivers/desktopDriver.ts
406
- var import_child_process = require("child_process");
500
+ // src/shared/drivers/previewDriver.ts
407
501
  var import_http = __toESM(require("http"));
408
- var import_path3 = __toESM(require("path"));
409
- var import_fs3 = __toESM(require("fs"));
502
+ var import_fs4 = __toESM(require("fs"));
503
+ var import_path4 = __toESM(require("path"));
410
504
 
411
505
  // src/shared/lib/playwrightLoader.ts
412
506
  var import_playwright = require("playwright");
413
507
  var chromium = new Proxy(import_playwright.chromium, {
414
508
  get(target, prop, receiver) {
415
509
  if (prop === "launch") {
416
- return async (...args2) => {
510
+ return async (...args) => {
417
511
  try {
418
- return await target.launch(...args2);
512
+ return await target.launch(...args);
419
513
  } catch (err) {
420
514
  if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
421
515
  console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
@@ -430,357 +524,6 @@ var chromium = new Proxy(import_playwright.chromium, {
430
524
  }
431
525
  });
432
526
 
433
- // src/shared/drivers/desktopDriver.ts
434
- var import_tree_kill = __toESM(require("tree-kill"));
435
- var DesktopDriver = class {
436
- port;
437
- executablePath;
438
- autoLaunch;
439
- args;
440
- env;
441
- cwd;
442
- childProcess = null;
443
- browser = null;
444
- context = null;
445
- page = null;
446
- processStderr = "";
447
- processExited = false;
448
- exitCode = null;
449
- constructor(options2) {
450
- this.port = options2?.port || 9222;
451
- this.autoLaunch = options2?.autoLaunch ?? true;
452
- this.executablePath = options2?.executablePath;
453
- this.args = options2?.args || [];
454
- this.env = options2?.env || {};
455
- this.cwd = options2?.cwd;
456
- }
457
- async isPortAvailable() {
458
- return new Promise((resolve) => {
459
- const req = import_http.default.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
460
- resolve(res.statusCode === 200);
461
- });
462
- req.on("error", () => resolve(false));
463
- req.setTimeout(800, () => {
464
- req.destroy();
465
- resolve(false);
466
- });
467
- });
468
- }
469
- async waitForPort(timeoutMs = 25e3) {
470
- const startTime = Date.now();
471
- while (Date.now() - startTime < timeoutMs) {
472
- if (this.processExited) {
473
- throw new Error(
474
- `[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
475
- Stderr: ${this.processStderr.trim() || "(none)"}`
476
- );
477
- }
478
- if (await this.isPortAvailable()) {
479
- return;
480
- }
481
- await new Promise((r) => setTimeout(r, 400));
482
- }
483
- throw new Error(
484
- `Timeout waiting for WebView2/Chromium CDP port ${this.port}. Last stderr:
485
- ${this.processStderr.trim() || "(no stderr output)"}`
486
- );
487
- }
488
- async start(initialViewport = { width: 1200, height: 800 }) {
489
- const alreadyRunning = await this.isPortAvailable();
490
- if (!alreadyRunning) {
491
- if (!this.autoLaunch) {
492
- throw new Error(
493
- `App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
494
- );
495
- }
496
- if (!this.executablePath) {
497
- throw new Error(
498
- `[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
499
- );
500
- }
501
- const resolvedExe = import_path3.default.resolve(process.cwd(), this.executablePath);
502
- if (!import_fs3.default.existsSync(resolvedExe)) {
503
- throw new Error(
504
- `[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
505
- );
506
- }
507
- console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
508
- const finalArgs = [...this.args];
509
- const hasDebugPort = finalArgs.some((a) => a.startsWith("--remote-debugging-port="));
510
- if (!hasDebugPort) {
511
- finalArgs.push(`--remote-debugging-port=${this.port}`);
512
- }
513
- const mergedEnv = {
514
- ...process.env,
515
- ...this.env,
516
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${this.port}`
517
- };
518
- this.processStderr = "";
519
- this.processExited = false;
520
- this.exitCode = null;
521
- this.childProcess = (0, import_child_process.spawn)(resolvedExe, finalArgs, {
522
- env: mergedEnv,
523
- cwd: this.cwd || import_path3.default.dirname(resolvedExe),
524
- stdio: ["ignore", "ignore", "pipe"],
525
- detached: false
526
- });
527
- ;
528
- this.processStderr = "";
529
- this.processExited = false;
530
- this.exitCode = null;
531
- this.childProcess = (0, import_child_process.spawn)(resolvedExe, this.args, {
532
- env: mergedEnv,
533
- cwd: this.cwd || import_path3.default.dirname(resolvedExe),
534
- stdio: ["ignore", "ignore", "pipe"],
535
- detached: false
536
- });
537
- this.childProcess.stderr?.on("data", (chunk) => {
538
- this.processStderr += chunk.toString();
539
- });
540
- this.childProcess.on("exit", (code) => {
541
- this.processExited = true;
542
- this.exitCode = code;
543
- });
544
- this.childProcess.on("error", (err) => {
545
- console.error("[DesktopDriver] Failed to spawn process:", err);
546
- });
547
- console.log(`[DesktopDriver] Waiting for CDP debugging port on ${this.port}...`);
548
- await this.waitForPort();
549
- } else {
550
- console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
551
- }
552
- console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
553
- this.browser = await chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
554
- const contexts = this.browser.contexts();
555
- this.context = contexts[0] || await this.browser.newContext();
556
- const pages = this.context.pages();
557
- if (pages.length > 0) {
558
- this.page = pages[0];
559
- } else {
560
- this.page = await this.context.waitForEvent("page", { timeout: 1e4 });
561
- }
562
- if (initialViewport) {
563
- await this.page.setViewportSize(initialViewport).catch(() => {
564
- });
565
- }
566
- return { page: this.page, context: this.context, browser: this.browser };
567
- }
568
- async stop() {
569
- if (this.browser) {
570
- await this.browser.close().catch(() => {
571
- });
572
- this.browser = null;
573
- }
574
- if (this.childProcess && !this.childProcess.killed && this.childProcess.pid) {
575
- console.log("[DesktopDriver] Terminating spawned desktop process tree...");
576
- const pid = this.childProcess.pid;
577
- await new Promise((resolve) => {
578
- (0, import_tree_kill.default)(pid, "SIGTERM", () => resolve());
579
- });
580
- this.childProcess = null;
581
- }
582
- }
583
- };
584
-
585
- // src/shared/drivers/previewDriver.ts
586
- var import_http2 = __toESM(require("http"));
587
- var import_fs5 = __toESM(require("fs"));
588
- var import_path5 = __toESM(require("path"));
589
-
590
- // src/features/mock-ipc/mockIpc.ts
591
- var MockIpcRegistry = class {
592
- mocks = /* @__PURE__ */ new Map();
593
- set(action, data, options2) {
594
- this.mocks.set(action, {
595
- action,
596
- data,
597
- type: options2?.type ?? "SUCCESS",
598
- delayMs: options2?.delayMs ?? 20
599
- });
600
- }
601
- setBatch(entries) {
602
- for (const entry of entries) {
603
- this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
604
- }
605
- }
606
- remove(action) {
607
- this.mocks.delete(action);
608
- }
609
- clear() {
610
- this.mocks.clear();
611
- }
612
- get(action) {
613
- return this.mocks.get(action) ?? null;
614
- }
615
- toSerializable() {
616
- const result = {};
617
- for (const [action, entry] of this.mocks.entries()) {
618
- result[action] = {
619
- data: entry.data,
620
- type: entry.type ?? "SUCCESS",
621
- delayMs: entry.delayMs ?? 20
622
- };
623
- }
624
- return result;
625
- }
626
- get size() {
627
- return this.mocks.size;
628
- }
629
- };
630
- function generateMockIpcScript(registry) {
631
- const mocksJson = JSON.stringify(registry.toSerializable());
632
- return `
633
- (() => {
634
- const __mockTable = ${mocksJson};
635
-
636
- window.__visualRunnerMocks = __mockTable;
637
-
638
- // Generic IPC mock bridge for modern web applications
639
- window.__mockIpc = {
640
- invoke: (action, payload) => {
641
- return new Promise((resolve, reject) => {
642
- const mock = window.__visualRunnerMocks[action];
643
-
644
- if (mock) {
645
- setTimeout(() => {
646
- if (mock.type === 'ERROR') {
647
- reject(new Error(mock.data));
648
- } else {
649
- resolve(mock.data);
650
- }
651
- }, mock.delayMs || 20);
652
- } else {
653
- console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
654
- setTimeout(() => resolve(null), 20);
655
- }
656
- });
657
- }
658
- };
659
-
660
- // Safe fallback bridge for hybrid webviews (Photino / CEF / WebView2)
661
- try {
662
- if (!window.external) {
663
- (window as any).external = {};
664
- }
665
- (window.external as any).sendMessage = (msg) => {
666
- try {
667
- const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg;
668
- const action = parsed.Action || parsed.action;
669
- const id = parsed.Id || parsed.id;
670
- const mock = (window as any).__visualRunnerMocks[action];
671
-
672
- if (mock) {
673
- const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
674
- setTimeout(() => {
675
- const cb = (window as any).__mockCallback;
676
- if (typeof cb === 'function') cb(JSON.stringify(response));
677
- }, mock.delayMs || 20);
678
- } else {
679
- setTimeout(() => {
680
- const cb = (window as any).__mockCallback;
681
- if (typeof cb === 'function') cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
682
- }, 20);
683
- }
684
- } catch (e) {
685
- console.error('[Mock IPC] Failed to process message:', e);
686
- }
687
- };
688
-
689
- (window.external as any).receiveMessage = (callback) => {
690
- (window as any).__mockCallback = callback;
691
- };
692
- } catch {
693
- // Ignored if window.external is read-only in strict Chromium sandboxes
694
- }
695
-
696
- console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys((window as any).__visualRunnerMocks).length, 'mocked actions');
697
- })();
698
-
699
- `;
700
- }
701
-
702
- // src/shared/lib/config.ts
703
- var import_path4 = __toESM(require("path"));
704
- var import_fs4 = __toESM(require("fs"));
705
- function loadConfig(cwd = process.cwd()) {
706
- const jsonConfigPath = import_path4.default.join(cwd, "agent-lens.json");
707
- if (import_fs4.default.existsSync(jsonConfigPath)) {
708
- try {
709
- const raw = import_fs4.default.readFileSync(jsonConfigPath, "utf-8");
710
- return JSON.parse(raw);
711
- } catch (e) {
712
- console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
713
- }
714
- }
715
- const packageJsonPath = import_path4.default.join(cwd, "package.json");
716
- if (import_fs4.default.existsSync(packageJsonPath)) {
717
- try {
718
- const raw = import_fs4.default.readFileSync(packageJsonPath, "utf-8");
719
- const pkg = JSON.parse(raw);
720
- if (pkg.agentLens && typeof pkg.agentLens === "object") {
721
- return pkg.agentLens;
722
- }
723
- } catch {
724
- }
725
- }
726
- return {};
727
- }
728
- function detectStartCwd(providedCwd) {
729
- if (providedCwd) {
730
- const resolved = import_path4.default.resolve(process.cwd(), providedCwd);
731
- if (import_fs4.default.existsSync(resolved)) {
732
- return providedCwd;
733
- }
734
- }
735
- const rootPkgPath = import_path4.default.join(process.cwd(), "package.json");
736
- let rootHasDevScript = false;
737
- if (import_fs4.default.existsSync(rootPkgPath)) {
738
- try {
739
- const rootPkg = JSON.parse(import_fs4.default.readFileSync(rootPkgPath, "utf-8"));
740
- rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
741
- } catch {
742
- }
743
- }
744
- if (rootHasDevScript) {
745
- return void 0;
746
- }
747
- const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
748
- for (const candidate of candidates) {
749
- const candidatePkg = import_path4.default.join(process.cwd(), candidate, "package.json");
750
- if (import_fs4.default.existsSync(candidatePkg)) {
751
- return `./${candidate}`;
752
- }
753
- }
754
- return void 0;
755
- }
756
- function resolveWwwrootDir(customDir) {
757
- if (customDir) {
758
- return import_path4.default.resolve(process.cwd(), customDir);
759
- }
760
- const candidates = [
761
- "dist",
762
- "build",
763
- "out",
764
- "wwwroot",
765
- "Frontend/dist",
766
- "frontend/dist",
767
- "client/dist"
768
- ];
769
- for (const c of candidates) {
770
- const candidatePath = import_path4.default.resolve(process.cwd(), c);
771
- if (import_fs4.default.existsSync(candidatePath) && import_fs4.default.existsSync(import_path4.default.join(candidatePath, "index.html"))) {
772
- return candidatePath;
773
- }
774
- }
775
- for (const c of candidates) {
776
- const candidatePath = import_path4.default.resolve(process.cwd(), c);
777
- if (import_fs4.default.existsSync(candidatePath)) {
778
- return candidatePath;
779
- }
780
- }
781
- return import_path4.default.resolve(process.cwd(), "dist");
782
- }
783
-
784
527
  // src/shared/drivers/previewDriver.ts
785
528
  var MIME_TYPES = {
786
529
  ".html": "text/html; charset=utf-8",
@@ -806,13 +549,11 @@ var PreviewDriver = class {
806
549
  options;
807
550
  _baseUrl = "";
808
551
  initialRouteMocks = [];
809
- mockRegistry;
810
- constructor(options2) {
811
- this.options = options2 || {};
552
+ constructor(options) {
553
+ this.options = options || {};
812
554
  if (!this.options.url) {
813
555
  this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
814
556
  }
815
- this.mockRegistry = new MockIpcRegistry();
816
557
  }
817
558
  get baseUrl() {
818
559
  return this._baseUrl;
@@ -820,25 +561,25 @@ var PreviewDriver = class {
820
561
  async start(initialViewport = { width: 1200, height: 800 }) {
821
562
  let targetUrl = this.options.url;
822
563
  if (!targetUrl) {
823
- if (!import_fs5.default.existsSync(this.options.wwwrootDir)) {
564
+ if (!import_fs4.default.existsSync(this.options.wwwrootDir)) {
824
565
  throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
825
566
  }
826
567
  await new Promise((resolve, reject) => {
827
- this.server = import_http2.default.createServer((req, res) => {
568
+ this.server = import_http.default.createServer((req, res) => {
828
569
  let reqUrl = req.url?.split("?")[0] || "/";
829
570
  if (reqUrl === "/") reqUrl = "/index.html";
830
- 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));
831
572
  if (!safePath.startsWith(this.options.wwwrootDir)) {
832
573
  res.writeHead(403);
833
574
  return res.end("Forbidden");
834
575
  }
835
- if (!import_fs5.default.existsSync(safePath) || import_fs5.default.statSync(safePath).isDirectory()) {
836
- 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");
837
578
  }
838
- const ext = import_path5.default.extname(safePath).toLowerCase();
579
+ const ext = import_path4.default.extname(safePath).toLowerCase();
839
580
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
840
581
  try {
841
- const content = import_fs5.default.readFileSync(safePath);
582
+ const content = import_fs4.default.readFileSync(safePath);
842
583
  res.writeHead(200, { "Content-Type": contentType });
843
584
  res.end(content);
844
585
  } catch (e) {
@@ -870,10 +611,6 @@ var PreviewDriver = class {
870
611
  viewport: initialViewport,
871
612
  deviceScaleFactor: 1
872
613
  });
873
- if (this.mockRegistry.size > 0) {
874
- const mockScript = generateMockIpcScript(this.mockRegistry);
875
- await this.context.addInitScript(mockScript);
876
- }
877
614
  this.page = await this.context.newPage();
878
615
  if (this.initialRouteMocks.length > 0) {
879
616
  for (const entry of this.initialRouteMocks) {
@@ -910,28 +647,6 @@ var PreviewDriver = class {
910
647
  await this.addRouteMock(r);
911
648
  }
912
649
  }
913
- async updateMockIpc(action, data, options2) {
914
- if (!this.page) {
915
- throw new Error("PreviewDriver not started. Call start() first.");
916
- }
917
- this.mockRegistry.set(action, data, options2);
918
- await this.page.evaluate(
919
- ({ action: action2, mock }) => {
920
- if (!window.__visualRunnerMocks) {
921
- window.__visualRunnerMocks = {};
922
- }
923
- window.__visualRunnerMocks[action2] = mock;
924
- },
925
- {
926
- action,
927
- mock: {
928
- data,
929
- type: options2?.type ?? "SUCCESS",
930
- delayMs: options2?.delayMs ?? 20
931
- }
932
- }
933
- );
934
- }
935
650
  async stop() {
936
651
  if (this.context) {
937
652
  await this.context.close().catch(() => {
@@ -953,17 +668,17 @@ var PreviewDriver = class {
953
668
  };
954
669
 
955
670
  // src/shared/lib/processManager.ts
956
- var import_child_process2 = require("child_process");
957
- var import_tree_kill2 = __toESM(require("tree-kill"));
671
+ var import_child_process = require("child_process");
672
+ var import_tree_kill = __toESM(require("tree-kill"));
958
673
  var ProcessManager = class {
959
674
  childProcess = null;
960
675
  isStopped = false;
961
- async start(command, options2) {
962
- const cwd = options2?.cwd || process.cwd();
676
+ async start(command, options) {
677
+ const cwd = options?.cwd || process.cwd();
963
678
  console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
964
- this.childProcess = (0, import_child_process2.spawn)(command, {
679
+ this.childProcess = (0, import_child_process.spawn)(command, {
965
680
  cwd,
966
- env: { ...process.env, ...options2?.env },
681
+ env: { ...process.env, ...options?.env },
967
682
  shell: true,
968
683
  stdio: ["ignore", "pipe", "pipe"]
969
684
  });
@@ -1012,10 +727,10 @@ var ProcessManager = class {
1012
727
  const pid = this.childProcess.pid;
1013
728
  console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
1014
729
  await new Promise((resolve) => {
1015
- (0, import_tree_kill2.default)(pid, "SIGTERM", (err) => {
730
+ (0, import_tree_kill.default)(pid, "SIGTERM", (err) => {
1016
731
  if (err) {
1017
732
  try {
1018
- (0, import_tree_kill2.default)(pid, "SIGKILL");
733
+ (0, import_tree_kill.default)(pid, "SIGKILL");
1019
734
  } catch {
1020
735
  }
1021
736
  }
@@ -1026,31 +741,438 @@ var ProcessManager = class {
1026
741
  }
1027
742
  };
1028
743
 
1029
- // src/features/runner/runner.ts
1030
- async function runVisualScenario(options2) {
1031
- const { scenario } = options2;
1032
- const targetMode = options2.targetMode || "preview";
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
+ }
760
+ }
761
+ cur = import_path5.default.dirname(cur);
762
+ }
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";
1033
1143
  const startTime = Date.now();
1034
1144
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1035
- const artifactsRoot = options2.artifactsRoot || import_path6.default.resolve(process.cwd(), "artifacts");
1036
- if (options2.cleanArtifacts && import_fs6.default.existsSync(artifactsRoot)) {
1145
+ const artifactsRoot = options.artifactsRoot || import_path7.default.resolve(process.cwd(), "artifacts");
1146
+ if (options.cleanArtifacts && import_fs7.default.existsSync(artifactsRoot)) {
1037
1147
  console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
1038
- const entries = import_fs6.default.readdirSync(artifactsRoot, { withFileTypes: true });
1148
+ const entries = import_fs7.default.readdirSync(artifactsRoot, { withFileTypes: true });
1039
1149
  for (const entry of entries) {
1040
- import_fs6.default.rmSync(import_path6.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
1150
+ import_fs7.default.rmSync(import_path7.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
1041
1151
  }
1042
1152
  }
1043
- const scenarioArtifactsDir = import_path6.default.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1044
- if (!import_fs6.default.existsSync(scenarioArtifactsDir)) {
1045
- import_fs6.default.mkdirSync(scenarioArtifactsDir, { recursive: true });
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 });
1046
1156
  }
1047
1157
  const captureEngine = new CaptureEngine(scenarioArtifactsDir);
1048
1158
  const consoleTracker = new ConsoleTracker();
1049
1159
  const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
1050
- let currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1051
- let desktopDriver = null;
1160
+ const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1052
1161
  let previewDriver = null;
1053
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
+ };
1054
1176
  try {
1055
1177
  let page;
1056
1178
  let context;
@@ -1061,38 +1183,37 @@ async function runVisualScenario(options2) {
1061
1183
  console.log(`\u{1F4C1} Artifacts: ${scenarioArtifactsDir}`);
1062
1184
  console.log(`========================================
1063
1185
  `);
1064
- if (options2.startCommand) {
1186
+ if (options.startCommand) {
1065
1187
  processManager = new ProcessManager();
1066
- await processManager.start(options2.startCommand, { cwd: options2.startCwd });
1067
- const waitTarget = options2.url || "http://localhost:5173";
1188
+ await processManager.start(options.startCommand, { cwd: options.startCwd });
1189
+ const waitTarget = options.url || "http://localhost:5173";
1068
1190
  await processManager.waitForUrl(waitTarget);
1069
1191
  }
1192
+ await pluginManager.runSetup(hookContext);
1070
1193
  if (typeof scenario.setup === "function") {
1071
1194
  console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
1072
1195
  await scenario.setup();
1073
1196
  }
1074
- if (targetMode === "desktop") {
1075
- desktopDriver = new DesktopDriver({
1076
- port: options2.port || 9222,
1077
- autoLaunch: options2.autoLaunchDesktop ?? true,
1078
- executablePath: options2.executablePath,
1079
- args: options2.desktopArgs,
1080
- env: options2.desktopEnv
1081
- });
1082
- const res = await desktopDriver.start(currentViewport);
1083
- page = res.page;
1084
- 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
+ );
1085
1211
  } else {
1086
1212
  previewDriver = new PreviewDriver({
1087
- wwwrootDir: options2.wwwrootDir,
1088
- url: options2.url,
1089
- headed: options2.headed
1213
+ wwwrootDir: options.wwwrootDir,
1214
+ url: options.url,
1215
+ headed: options.headed
1090
1216
  });
1091
- const mergedMocks = [...options2.globalMocks || [], ...scenario.mockIpc || []];
1092
- if (mergedMocks.length > 0) {
1093
- console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
1094
- previewDriver.mockRegistry.setBatch(mergedMocks);
1095
- }
1096
1217
  if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1097
1218
  console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
1098
1219
  await previewDriver.setupRouteMocks(scenario.mockRoutes);
@@ -1104,194 +1225,27 @@ async function runVisualScenario(options2) {
1104
1225
  await previewDriver.setupRouteMocks(scenario.mockRoutes);
1105
1226
  }
1106
1227
  }
1228
+ await pluginManager.runOnContextCreated(context, hookContext);
1229
+ await pluginManager.runOnPageCreated(page, context, hookContext);
1107
1230
  consoleTracker.attach(page);
1108
1231
  console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
1109
1232
  `);
1110
- const doNavigate = async (route) => {
1111
- if (route.startsWith("http://") || route.startsWith("https://")) {
1112
- await page.goto(route, { waitUntil: "domcontentloaded" });
1113
- await page.waitForTimeout(300);
1114
- return;
1115
- }
1116
- if (route.startsWith("#")) {
1117
- await page.evaluate((r) => {
1118
- window.location.hash = r;
1119
- }, route);
1120
- await page.waitForTimeout(300);
1121
- return;
1122
- }
1123
- if (previewDriver?.baseUrl && options2.url) {
1124
- try {
1125
- const fullUrl = new URL(route, previewDriver.baseUrl).toString();
1126
- await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
1127
- await page.waitForTimeout(300);
1128
- return;
1129
- } catch {
1130
- }
1131
- }
1132
- const routePath = route.startsWith("/") ? route : `/${route}`;
1133
- await page.evaluate((r) => {
1134
- if (window.location.hash !== void 0) {
1135
- window.location.hash = r;
1136
- }
1137
- }, routePath);
1138
- await page.waitForTimeout(300);
1139
- };
1233
+ const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
1140
1234
  if (scenario.route) {
1141
- console.log(`[Runner] Navigating to route: ${scenario.route}`);
1235
+ console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
1142
1236
  await doNavigate(scenario.route);
1143
1237
  }
1144
- const ctx = {
1238
+ const { ctx } = buildTestContext({
1145
1239
  page,
1146
1240
  context,
1147
1241
  targetMode,
1148
- currentViewport,
1149
- capture: async (name, opts) => {
1150
- console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
1151
- return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
1152
- },
1153
- captureBurst: async (name, opts) => {
1154
- console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
1155
- return await captureEngine.takeBurst(page, name, currentViewport, opts);
1156
- },
1157
- navigate: async (route) => {
1158
- console.log(`\u{1F9ED} [Navigate] ${route}`);
1159
- await doNavigate(route);
1160
- },
1161
- // ─── Viewport ───
1162
- resize: async (width, height) => {
1163
- console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1164
- currentViewport = { width, height };
1165
- ctx.currentViewport = currentViewport;
1166
- await page.setViewportSize(currentViewport);
1167
- await page.waitForTimeout(200);
1168
- },
1169
- setPreset: async (preset) => {
1170
- console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1171
- await ctx.resize(preset.width, preset.height);
1172
- },
1173
- resizeToFit: async (selector, padding = 0) => {
1174
- let boundingBox;
1175
- if (selector) {
1176
- const el = await page.$(selector);
1177
- if (el) {
1178
- boundingBox = await el.boundingBox();
1179
- }
1180
- } else {
1181
- boundingBox = await page.evaluate(() => {
1182
- return {
1183
- width: document.documentElement.scrollWidth,
1184
- height: document.documentElement.scrollHeight
1185
- };
1186
- });
1187
- }
1188
- if (boundingBox) {
1189
- const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1190
- const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1191
- console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1192
- await ctx.resize(newWidth, newHeight);
1193
- } else {
1194
- console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1195
- }
1196
- },
1197
- wait: async (ms) => {
1198
- await page.waitForTimeout(ms);
1199
- },
1200
- waitForSelector: async (selector, timeoutMs = 5e3) => {
1201
- await page.waitForSelector(selector, { timeout: timeoutMs });
1202
- },
1203
- click: async (selector) => {
1204
- console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1205
- await page.click(selector);
1206
- },
1207
- rightClick: async (selector) => {
1208
- console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1209
- await page.click(selector, { button: "right" });
1210
- },
1211
- type: async (selector, text) => {
1212
- console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1213
- await page.fill(selector, text);
1214
- },
1215
- selectOption: async (selector, value) => {
1216
- console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1217
- await page.selectOption(selector, value);
1218
- },
1219
- hover: async (selector) => {
1220
- console.log(`\u{1F446} [Hover] ${selector}`);
1221
- await page.hover(selector);
1222
- },
1223
- scroll: async (selector, deltaY) => {
1224
- console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1225
- await page.evaluate(({ sel, dY }) => {
1226
- const el = document.querySelector(sel);
1227
- if (el) {
1228
- el.scrollTop += dY;
1229
- } else {
1230
- window.scrollBy(0, dY);
1231
- }
1232
- }, { sel: selector, dY: deltaY });
1233
- await page.waitForTimeout(100);
1234
- },
1235
- log: (msg) => {
1236
- console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1237
- },
1238
- // ─── Mock IPC ───
1239
- setMockIpc: async (action, data, mockOptions) => {
1240
- if (targetMode === "desktop") {
1241
- console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
1242
- return;
1243
- }
1244
- if (!previewDriver) {
1245
- console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
1246
- return;
1247
- }
1248
- console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
1249
- await previewDriver.updateMockIpc(action, data, mockOptions);
1250
- },
1251
- setMockRoute: async (url, body, routeOptions) => {
1252
- if (targetMode === "desktop") {
1253
- console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored in desktop mode`);
1254
- return;
1255
- }
1256
- if (!previewDriver) {
1257
- console.log(`\u26A0\uFE0F [Mock Route] PreviewDriver not available`);
1258
- return;
1259
- }
1260
- console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
1261
- await previewDriver.addRouteMock({
1262
- url,
1263
- body,
1264
- method: routeOptions?.method,
1265
- status: routeOptions?.status,
1266
- delayMs: routeOptions?.delayMs,
1267
- headers: routeOptions?.headers
1268
- });
1269
- },
1270
- getConsoleErrors: () => consoleTracker.getErrors(),
1271
- getConsoleWarnings: () => consoleTracker.getWarnings(),
1272
- hasConsoleErrors: () => consoleTracker.hasErrors,
1273
- // ─── DOM Assertions ───
1274
- readText: async (selector) => {
1275
- const text = await page.textContent(selector);
1276
- return text ? text.trim() : null;
1277
- },
1278
- getPageText: async () => {
1279
- return await page.evaluate(() => document.body.innerText || "");
1280
- },
1281
- isVisible: async (selector) => {
1282
- try {
1283
- const element = await page.$(selector);
1284
- if (!element) return false;
1285
- return await element.isVisible();
1286
- } catch {
1287
- return false;
1288
- }
1289
- },
1290
- getElementCount: async (selector) => {
1291
- const elements = await page.$$(selector);
1292
- return elements.length;
1293
- }
1294
- };
1242
+ initialViewport: currentViewport,
1243
+ captureEngine,
1244
+ consoleTracker,
1245
+ previewDriver,
1246
+ doNavigate
1247
+ });
1248
+ await pluginManager.extendContext(ctx, page, hookContext);
1295
1249
  await scenario.run(ctx);
1296
1250
  const durationMs = Date.now() - startTime;
1297
1251
  const snapshots = captureEngine.getSnapshots();
@@ -1306,38 +1260,19 @@ async function runVisualScenario(options2) {
1306
1260
  console.log(`
1307
1261
  \u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
1308
1262
  }
1309
- const reportPath = VisualReporter.generateReport({
1263
+ const reportData = {
1310
1264
  scenario,
1311
1265
  snapshots,
1312
1266
  consoleErrors,
1313
1267
  consoleWarnings,
1314
1268
  outputDir: scenarioArtifactsDir,
1315
1269
  targetMode,
1316
- durationMs
1317
- });
1318
- const syncLatest = (repPath, snaps) => {
1319
- try {
1320
- const latestDir = import_path6.default.join(artifactsRoot, "latest");
1321
- if (import_fs6.default.existsSync(latestDir)) {
1322
- import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
1323
- }
1324
- import_fs6.default.mkdirSync(latestDir, { recursive: true });
1325
- import_fs6.default.copyFileSync(repPath, import_path6.default.join(latestDir, "report.md"));
1326
- const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
1327
- if (import_fs6.default.existsSync(manifestSrc)) {
1328
- import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
1329
- }
1330
- for (const snap of snaps) {
1331
- if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
1332
- import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
1333
- }
1334
- }
1335
- return import_path6.default.join(latestDir, "report.md");
1336
- } catch {
1337
- return void 0;
1338
- }
1270
+ durationMs,
1271
+ customSections: []
1339
1272
  };
1340
- 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);
1341
1276
  console.log(`
1342
1277
  \u2705 Visual Test Completed Successfully!`);
1343
1278
  console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
@@ -1364,7 +1299,7 @@ async function runVisualScenario(options2) {
1364
1299
  const snapshots = captureEngine.getSnapshots();
1365
1300
  const consoleErrors = consoleTracker.getErrors();
1366
1301
  const consoleWarnings = consoleTracker.getWarnings();
1367
- const reportPath = VisualReporter.generateReport({
1302
+ const reportData = {
1368
1303
  scenario,
1369
1304
  snapshots,
1370
1305
  consoleErrors,
@@ -1372,16 +1307,9 @@ async function runVisualScenario(options2) {
1372
1307
  outputDir: scenarioArtifactsDir,
1373
1308
  targetMode,
1374
1309
  durationMs
1375
- });
1376
- try {
1377
- const latestDir = import_path6.default.join(artifactsRoot, "latest");
1378
- if (import_fs6.default.existsSync(latestDir)) {
1379
- import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
1380
- }
1381
- import_fs6.default.mkdirSync(latestDir, { recursive: true });
1382
- import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
1383
- } catch {
1384
- }
1310
+ };
1311
+ const reportPath = VisualReporter.generateReport(reportData);
1312
+ syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1385
1313
  return {
1386
1314
  scenarioId: scenario.id,
1387
1315
  targetMode,
@@ -1394,6 +1322,7 @@ async function runVisualScenario(options2) {
1394
1322
  error: err.message
1395
1323
  };
1396
1324
  } finally {
1325
+ await pluginManager.runTeardown(hookContext);
1397
1326
  if (typeof scenario.teardown === "function") {
1398
1327
  try {
1399
1328
  console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
@@ -1402,21 +1331,21 @@ async function runVisualScenario(options2) {
1402
1331
  console.error(`\u26A0\uFE0F [Scenario Teardown] Error during teardown:`, teardownErr.message);
1403
1332
  }
1404
1333
  }
1405
- if (options2.cleanPaths && options2.cleanPaths.length > 0) {
1406
- for (const cleanTarget of options2.cleanPaths) {
1334
+ if (options.cleanPaths && options.cleanPaths.length > 0) {
1335
+ for (const cleanTarget of options.cleanPaths) {
1407
1336
  try {
1408
- const resolvedCleanPath = import_path6.default.resolve(process.cwd(), cleanTarget);
1409
- if (import_fs6.default.existsSync(resolvedCleanPath)) {
1337
+ const resolvedCleanPath = import_path7.default.resolve(process.cwd(), cleanTarget);
1338
+ if (import_fs7.default.existsSync(resolvedCleanPath)) {
1410
1339
  console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
1411
- import_fs6.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
1340
+ import_fs7.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
1412
1341
  }
1413
1342
  } catch (cleanErr) {
1414
1343
  console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
1415
1344
  }
1416
1345
  }
1417
1346
  }
1418
- if (!options2.detach) {
1419
- if (desktopDriver) await desktopDriver.stop();
1347
+ if (!options.detach) {
1348
+ if (customDriverStop) await customDriverStop();
1420
1349
  if (previewDriver) await previewDriver.stop();
1421
1350
  if (processManager) await processManager.stop();
1422
1351
  } else {
@@ -1425,12 +1354,9 @@ async function runVisualScenario(options2) {
1425
1354
  }
1426
1355
  }
1427
1356
 
1428
- // src/app/cli.ts
1429
- var import_jiti = require("jiti");
1430
-
1431
1357
  // src/features/snap/snap.ts
1432
- var import_path7 = __toESM(require("path"));
1433
- var import_fs7 = __toESM(require("fs"));
1358
+ var import_path8 = __toESM(require("path"));
1359
+ var import_fs8 = __toESM(require("fs"));
1434
1360
  var PRESET_MAP = {
1435
1361
  default: VIEWPORT_PRESETS.DEFAULT,
1436
1362
  desktop: VIEWPORT_PRESETS.DEFAULT,
@@ -1477,18 +1403,18 @@ async function detectActiveDevServer() {
1477
1403
  }
1478
1404
  return null;
1479
1405
  }
1480
- async function runQuickSnap(options2) {
1481
- let targetUrl = options2.url;
1406
+ async function runQuickSnap(options) {
1407
+ let targetUrl = options.url;
1482
1408
  let useStaticPreview = false;
1483
1409
  let wwwrootDir;
1484
- if (!targetUrl && !options2.start) {
1410
+ if (!targetUrl && !options.start) {
1485
1411
  const activeUrl = await detectActiveDevServer();
1486
1412
  if (activeUrl) {
1487
1413
  targetUrl = activeUrl;
1488
1414
  console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
1489
1415
  } else {
1490
1416
  const candidateDir = resolveWwwrootDir();
1491
- 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"))) {
1492
1418
  useStaticPreview = true;
1493
1419
  wwwrootDir = candidateDir;
1494
1420
  console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
@@ -1503,17 +1429,17 @@ async function runQuickSnap(options2) {
1503
1429
  return false;
1504
1430
  }
1505
1431
  }
1506
- } else if (!targetUrl && options2.start) {
1432
+ } else if (!targetUrl && options.start) {
1507
1433
  targetUrl = "http://localhost:5173";
1508
1434
  }
1509
- const waitMs = options2.waitMs ?? 1e3;
1510
- const snapshotPrefix = options2.name || "quick_snap";
1511
- const targetViewports = parseViewportPresets(options2.viewports);
1435
+ const waitMs = options.waitMs ?? 1e3;
1436
+ const snapshotPrefix = options.name || "quick_snap";
1437
+ const targetViewports = parseViewportPresets(options.viewports);
1512
1438
  console.log(`
1513
1439
  \u{1F4F8} [Quick Snap] Preparing instant verification for: ${targetUrl || wwwrootDir}`);
1514
1440
  console.log(`\u{1F4D0} [Quick Snap] Testing ${targetViewports.length} viewports: ${targetViewports.map((v) => `${v.name} (${v.width}x${v.height})`).join(", ")}`);
1515
- if (options2.selector) {
1516
- 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}"`);
1517
1443
  }
1518
1444
  const snapScenario = defineVisualTest({
1519
1445
  id: "quick-snap",
@@ -1530,16 +1456,16 @@ async function runQuickSnap(options2) {
1530
1456
  ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
1531
1457
  await ctx.setPreset(vp);
1532
1458
  await ctx.wait(200);
1533
- await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`);
1459
+ await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
1534
1460
  }
1535
- if (options2.selector) {
1536
- ctx.log(`Focusing on selector: "${options2.selector}"`);
1461
+ if (options.selector) {
1462
+ ctx.log(`Focusing on selector: "${options.selector}"`);
1537
1463
  try {
1538
1464
  await ctx.setPreset(VIEWPORT_PRESETS.DEFAULT);
1539
- await ctx.resizeToFit(options2.selector, 15);
1540
- 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 });
1541
1467
  } catch (err) {
1542
- 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}`);
1543
1469
  }
1544
1470
  }
1545
1471
  const errors = ctx.getConsoleErrors();
@@ -1552,18 +1478,19 @@ async function runQuickSnap(options2) {
1552
1478
  });
1553
1479
  const result = await runVisualScenario({
1554
1480
  scenario: snapScenario,
1555
- targetMode: options2.mode || "preview",
1481
+ targetMode: options.mode || "preview",
1556
1482
  url: useStaticPreview ? void 0 : targetUrl,
1557
1483
  wwwrootDir: useStaticPreview ? wwwrootDir : void 0,
1558
- startCommand: options2.start,
1559
- startCwd: options2.startCwd,
1560
- executablePath: options2.exe,
1561
- cleanPaths: options2.clean,
1562
- cleanArtifacts: options2.cleanArtifacts,
1563
- port: options2.port,
1564
- headed: options2.headed,
1565
- detach: options2.detach,
1566
- 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
1567
1494
  });
1568
1495
  console.log(`
1569
1496
  ========================================`);
@@ -1577,100 +1504,112 @@ async function runQuickSnap(options2) {
1577
1504
  return result.success && result.consoleErrors === 0;
1578
1505
  }
1579
1506
 
1580
- // src/app/cli.ts
1581
- var fileConfig = loadConfig();
1582
- var args = process.argv.slice(2);
1583
- if (args[0] === "init") {
1584
- initScenarioTemplate();
1585
- process.exit(0);
1586
- }
1587
- var isSnapCommand = args[0] === "snap";
1588
- var effectiveArgs = isSnapCommand ? args.slice(1) : args;
1589
- var options = {
1590
- mode: fileConfig.mode || "preview",
1591
- port: fileConfig.port || 9222,
1592
- headed: fileConfig.headed ?? false,
1593
- detach: fileConfig.detach ?? false,
1594
- build: fileConfig.buildCommand || false,
1595
- start: fileConfig.startCommand,
1596
- startCwd: fileConfig.startCwd,
1597
- cleanArtifacts: fileConfig.cleanArtifacts ?? false,
1598
- url: fileConfig.url,
1599
- exe: fileConfig.executablePath,
1600
- clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
1601
- dir: fileConfig.scenarios,
1602
- wwwroot: fileConfig.wwwroot,
1603
- outDir: fileConfig.outDir
1604
- };
1605
- for (const arg of effectiveArgs) {
1606
- if (arg === "--help" || arg === "-h") {
1607
- options.help = true;
1608
- } else if (arg.startsWith("--scenario=")) {
1609
- options.scenario = arg.split("=")[1];
1610
- } else if (arg === "--all") {
1611
- options.all = true;
1612
- } else if (arg.startsWith("--mode=")) {
1613
- const mode = arg.split("=")[1].toLowerCase();
1614
- if (mode === "desktop" || mode === "preview") {
1615
- options.mode = mode;
1616
- }
1617
- } else if (arg.startsWith("--port=")) {
1618
- options.port = parseInt(arg.split("=")[1], 10) || 9222;
1619
- } else if (arg === "--headed") {
1620
- options.headed = true;
1621
- } else if (arg === "--detach") {
1622
- options.detach = true;
1623
- } else if (arg === "--build") {
1624
- options.build = true;
1625
- } else if (arg.startsWith("--build=")) {
1626
- options.build = arg.slice("--build=".length);
1627
- } else if (arg.startsWith("--start=")) {
1628
- options.start = arg.slice("--start=".length);
1629
- } else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
1630
- options.startCwd = arg.split("=")[1];
1631
- } else if (arg === "--clean-artifacts") {
1632
- options.cleanArtifacts = true;
1633
- } else if (arg.startsWith("--url=")) {
1634
- options.url = arg.split("=")[1];
1635
- } else if (arg.startsWith("--selector=")) {
1636
- options.selector = arg.split("=")[1];
1637
- } else if (arg.startsWith("--viewports=")) {
1638
- const raw = arg.slice("--viewports=".length);
1639
- options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
1640
- } else if (arg.startsWith("--wait=")) {
1641
- options.waitMs = parseInt(arg.slice("--wait=".length), 10);
1642
- } else if (arg.startsWith("--name=")) {
1643
- options.name = arg.slice("--name=".length);
1644
- } else if (arg.startsWith("--exe=")) {
1645
- options.exe = arg.slice("--exe=".length);
1646
- } else if (arg.startsWith("--executable=")) {
1647
- options.exe = arg.slice("--executable=".length);
1648
- } else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
1649
- const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
1650
- const rawPaths = arg.slice(prefix.length);
1651
- options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
1652
- } else if (arg.startsWith("--dir=")) {
1653
- options.dir = arg.split("=")[1];
1654
- } else if (arg.startsWith("--wwwroot=")) {
1655
- options.wwwroot = arg.split("=")[1];
1656
- } else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
1657
- 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
+ }
1658
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
+ };
1659
1597
  }
1660
- if (options.start && !options.startCwd) {
1661
- options.startCwd = detectStartCwd(options.startCwd);
1662
- }
1598
+
1599
+ // src/app/lib/help.ts
1663
1600
  function printHelp() {
1664
1601
  console.log(`
1665
1602
  \u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
1666
1603
 
1667
1604
  Usage:
1668
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)
1669
1607
  npx agent-lens [options] Run scripted scenario tests from scenarios/
1670
1608
  npx agent-lens init Generate starter scenario template & mocks
1671
1609
 
1672
1610
  Commands:
1673
1611
  snap Take immediate multi-viewport screenshots of a URL & check console errors
1612
+ live Interactive control: click coordinates/selectors, type text, snap --full
1674
1613
  init Generate starter template in scenarios/template.scenario.ts and mocks.ts
1675
1614
 
1676
1615
  Options:
@@ -1678,6 +1617,7 @@ Options:
1678
1617
  --start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
1679
1618
  --start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
1680
1619
  --clean-artifacts Purge previous test artifacts to prevent folder bloat
1620
+ --full Capture full scrollable page instead of only the viewport
1681
1621
  --selector=<css> Target a specific element to focus on / resize-to-fit
1682
1622
  --viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
1683
1623
  --wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
@@ -1689,6 +1629,7 @@ Options:
1689
1629
  --port=<port> CDP remote debugging port [default: 9222]
1690
1630
  --build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
1691
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)
1692
1633
  --headed Show Chromium browser window
1693
1634
  --detach Keep browser/app open after finishing
1694
1635
  --dir=<path> Custom scenarios directory [default: scenarios]
@@ -1697,13 +1638,17 @@ Options:
1697
1638
  --help, -h Show this help message
1698
1639
  `);
1699
1640
  }
1641
+
1642
+ // src/app/lib/templateInit.ts
1643
+ var import_path9 = __toESM(require("path"));
1644
+ var import_fs9 = __toESM(require("fs"));
1700
1645
  function initScenarioTemplate() {
1701
- const targetDir = import_path8.default.resolve(process.cwd(), "scenarios");
1702
- if (!import_fs8.default.existsSync(targetDir)) {
1703
- 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 });
1704
1649
  }
1705
- const templatePath = import_path8.default.join(targetDir, "template.scenario.ts");
1706
- if (!import_fs8.default.existsSync(templatePath)) {
1650
+ const templatePath = import_path9.default.join(targetDir, "template.scenario.ts");
1651
+ if (!import_fs9.default.existsSync(templatePath)) {
1707
1652
  const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
1708
1653
 
1709
1654
  export default defineVisualTest({
@@ -1734,13 +1679,13 @@ export default defineVisualTest({
1734
1679
  }
1735
1680
  });
1736
1681
  `;
1737
- import_fs8.default.writeFileSync(templatePath, templateContent, "utf8");
1682
+ import_fs9.default.writeFileSync(templatePath, templateContent, "utf8");
1738
1683
  console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
1739
1684
  } else {
1740
1685
  console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
1741
1686
  }
1742
- const mocksPath = import_path8.default.join(targetDir, "mocks.ts");
1743
- if (!import_fs8.default.existsSync(mocksPath)) {
1687
+ const mocksPath = import_path9.default.join(targetDir, "mocks.ts");
1688
+ if (!import_fs9.default.existsSync(mocksPath)) {
1744
1689
  const mocksContent = `/**
1745
1690
  * Global IPC & API Mocks
1746
1691
  *
@@ -1751,21 +1696,21 @@ export default [
1751
1696
  { action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
1752
1697
  ];
1753
1698
  `;
1754
- import_fs8.default.writeFileSync(mocksPath, mocksContent, "utf8");
1699
+ import_fs9.default.writeFileSync(mocksPath, mocksContent, "utf8");
1755
1700
  console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
1756
1701
  }
1757
1702
  console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
1758
1703
  }
1759
- if (options.help) {
1760
- printHelp();
1761
- process.exit(0);
1762
- }
1704
+
1705
+ // src/app/lib/scenarioFinder.ts
1706
+ var import_path10 = __toESM(require("path"));
1707
+ var import_fs10 = __toESM(require("fs"));
1763
1708
  async function findScenarios(dir, specificName) {
1764
1709
  const results = [];
1765
- if (!import_fs8.default.existsSync(dir)) return results;
1766
- 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 });
1767
1712
  for (const item of items) {
1768
- const fullPath = import_path8.default.join(dir, item.name);
1713
+ const fullPath = import_path10.default.join(dir, item.name);
1769
1714
  if (item.isDirectory()) {
1770
1715
  results.push(...await findScenarios(fullPath, specificName));
1771
1716
  } else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
@@ -1778,7 +1723,7 @@ async function findScenarios(dir, specificName) {
1778
1723
  }
1779
1724
  function resolveScenariosDirectory(customDir) {
1780
1725
  if (customDir) {
1781
- return import_path8.default.resolve(process.cwd(), customDir);
1726
+ return import_path10.default.resolve(process.cwd(), customDir);
1782
1727
  }
1783
1728
  const candidates = [
1784
1729
  "scenarios",
@@ -1788,121 +1733,333 @@ function resolveScenariosDirectory(customDir) {
1788
1733
  "src/scenarios"
1789
1734
  ];
1790
1735
  for (const c of candidates) {
1791
- const p = import_path8.default.resolve(process.cwd(), c);
1792
- if (import_fs8.default.existsSync(p)) {
1736
+ const p = import_path10.default.resolve(process.cwd(), c);
1737
+ if (import_fs10.default.existsSync(p)) {
1793
1738
  return p;
1794
1739
  }
1795
1740
  }
1796
- 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
+ }
1797
1781
  }
1798
- async function main() {
1799
- if (isSnapCommand || options.url && !options.scenario && !options.all) {
1800
- const snapSuccess = await runQuickSnap({
1801
- url: options.url,
1802
- start: options.start,
1803
- startCwd: options.startCwd,
1804
- selector: options.selector,
1805
- viewports: options.viewports,
1806
- waitMs: options.waitMs,
1807
- name: options.name,
1808
- clean: options.clean,
1809
- cleanArtifacts: options.cleanArtifacts,
1810
- headed: options.headed,
1811
- detach: options.detach,
1812
- outDir: options.outDir,
1813
- mode: options.mode,
1814
- exe: options.exe,
1815
- port: options.port
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 };
1795
+ }
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(() => {
1816
1805
  });
1817
- process.exit(snapSuccess ? 0 : 1);
1806
+ } catch {
1818
1807
  }
1819
- if (!options.scenario && !options.all) {
1820
- console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
1821
- printHelp();
1822
- process.exit(1);
1808
+ if (session.pid) {
1809
+ await new Promise((resolve) => {
1810
+ (0, import_tree_kill2.default)(session.pid, "SIGTERM", () => resolve());
1811
+ });
1823
1812
  }
1824
- const scenariosDir = resolveScenariosDirectory(options.dir);
1825
- const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
1826
- if (scenarioPaths.length === 0) {
1827
- console.error(`\u274C No scenarios found in ${scenariosDir}`);
1828
- console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
1829
- 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 });
1830
1833
  }
1831
- const shouldBuild = options.build || fileConfig.autoBuild;
1832
- if (shouldBuild) {
1833
- const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
1834
- console.log(`
1835
- \u{1F528} [Build] Running build process: "${buildCmd}"...`);
1836
- try {
1837
- (0, import_child_process3.execSync)(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
1838
- } catch (e) {
1839
- console.error(`\u274C Build failed: ${buildCmd}`);
1840
- 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;
1841
1867
  }
1842
- } else if (options.mode === "preview" && !options.url && !options.start) {
1843
- console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
1844
- }
1845
- console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
1846
- const jiti = (0, import_jiti.createJiti)(process.cwd());
1847
- let globalMocks = [];
1848
- const mockCandidates = [
1849
- import_path8.default.join(scenariosDir, "mocks.ts"),
1850
- import_path8.default.join(scenariosDir, "mocks.js"),
1851
- import_path8.default.join(process.cwd(), "mocks.ts"),
1852
- import_path8.default.join(process.cwd(), "mocks.js")
1853
- ];
1854
- for (const mocksPath of mockCandidates) {
1855
- if (import_fs8.default.existsSync(mocksPath)) {
1856
- try {
1857
- const m = await jiti.import(mocksPath);
1858
- globalMocks = m.default || m.mocks || [];
1859
- console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path8.default.basename(mocksPath)}`);
1860
- break;
1861
- } catch (err) {
1862
- 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;
1863
1883
  }
1884
+ await snapLive(page, { name: "current" });
1885
+ return true;
1864
1886
  }
1865
- }
1866
- let allSuccess = true;
1867
- for (const scenarioPath of scenarioPaths) {
1868
- try {
1869
- const scenarioModule = await jiti.import(scenarioPath);
1870
- const scenario = scenarioModule.default || scenarioModule.scenario;
1871
- if (!scenario || typeof scenario.run !== "function") {
1872
- console.error(`\u26A0\uFE0F Skipped: file ${import_path8.default.basename(scenarioPath)} does not export a VisualScenario object by default.`);
1873
- 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;
1874
1895
  }
1875
- const result = await runVisualScenario({
1876
- scenario,
1877
- 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({
1878
1947
  url: options.url,
1879
- startCommand: options.start,
1948
+ start: options.start,
1880
1949
  startCwd: options.startCwd,
1950
+ selector: options.selector,
1951
+ viewports: options.viewports,
1952
+ waitMs: options.waitMs,
1953
+ name: options.name,
1954
+ clean: options.clean,
1881
1955
  cleanArtifacts: options.cleanArtifacts,
1882
- port: options.port,
1883
1956
  headed: options.headed,
1884
1957
  detach: options.detach,
1885
- executablePath: options.exe,
1886
- cleanPaths: options.clean,
1887
- desktopEnv: fileConfig.env,
1888
- wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
1889
- artifactsRoot: options.outDir ? import_path8.default.resolve(process.cwd(), options.outDir) : void 0,
1890
- 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
1891
1964
  });
1892
- 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);
1893
2053
  allSuccess = false;
1894
2054
  }
1895
- } catch (err) {
1896
- console.error(`\u274C Failed to load scenario ${import_path8.default.basename(scenarioPath)}:`, err);
1897
- allSuccess = false;
2055
+ }
2056
+ if (!allSuccess) {
2057
+ process.exit(1);
1898
2058
  }
1899
2059
  }
1900
- if (!allSuccess) {
2060
+ main().catch((err) => {
2061
+ console.error("Fatal error:", err);
1901
2062
  process.exit(1);
1902
- }
2063
+ });
1903
2064
  }
1904
- main().catch((err) => {
1905
- console.error("Fatal error:", err);
1906
- process.exit(1);
1907
- });
1908
2065
  //# sourceMappingURL=cli.js.map