@_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.mjs CHANGED
@@ -6,6 +6,114 @@ var __commonJS = (cb, mod) => function __require() {
6
6
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
7
7
  };
8
8
 
9
+ // src/shared/lib/config.ts
10
+ import path from "path";
11
+ import fs from "fs";
12
+ function loadConfig(cwd = process.cwd()) {
13
+ const jsonConfigPath = path.join(cwd, "agent-lens.json");
14
+ if (fs.existsSync(jsonConfigPath)) {
15
+ try {
16
+ const raw = fs.readFileSync(jsonConfigPath, "utf-8");
17
+ return JSON.parse(raw);
18
+ } catch (e) {
19
+ console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
20
+ }
21
+ }
22
+ const packageJsonPath = path.join(cwd, "package.json");
23
+ if (fs.existsSync(packageJsonPath)) {
24
+ try {
25
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
26
+ const pkg = JSON.parse(raw);
27
+ if (pkg.agentLens && typeof pkg.agentLens === "object") {
28
+ return pkg.agentLens;
29
+ }
30
+ } catch {
31
+ }
32
+ }
33
+ return {};
34
+ }
35
+ function detectStartCwd(providedCwd) {
36
+ if (providedCwd) {
37
+ const resolved = path.resolve(process.cwd(), providedCwd);
38
+ if (fs.existsSync(resolved)) {
39
+ return providedCwd;
40
+ }
41
+ }
42
+ const rootPkgPath = path.join(process.cwd(), "package.json");
43
+ let rootHasDevScript = false;
44
+ if (fs.existsSync(rootPkgPath)) {
45
+ try {
46
+ const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
47
+ rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
48
+ } catch {
49
+ }
50
+ }
51
+ if (rootHasDevScript) {
52
+ return void 0;
53
+ }
54
+ const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
55
+ for (const candidate of candidates) {
56
+ const candidatePkg = path.join(process.cwd(), candidate, "package.json");
57
+ if (fs.existsSync(candidatePkg)) {
58
+ return `./${candidate}`;
59
+ }
60
+ }
61
+ return void 0;
62
+ }
63
+ function resolveWwwrootDir(customDir) {
64
+ if (customDir) {
65
+ return path.resolve(process.cwd(), customDir);
66
+ }
67
+ const candidates = [
68
+ "dist",
69
+ "build",
70
+ "out",
71
+ "wwwroot",
72
+ "Frontend/dist",
73
+ "frontend/dist",
74
+ "client/dist"
75
+ ];
76
+ for (const c of candidates) {
77
+ const candidatePath = path.resolve(process.cwd(), c);
78
+ if (fs.existsSync(candidatePath) && fs.existsSync(path.join(candidatePath, "index.html"))) {
79
+ return candidatePath;
80
+ }
81
+ }
82
+ for (const c of candidates) {
83
+ const candidatePath = path.resolve(process.cwd(), c);
84
+ if (fs.existsSync(candidatePath)) {
85
+ return candidatePath;
86
+ }
87
+ }
88
+ return path.resolve(process.cwd(), "dist");
89
+ }
90
+ var init_config = __esm({
91
+ "src/shared/lib/config.ts"() {
92
+ "use strict";
93
+ }
94
+ });
95
+
96
+ // src/shared/api/plugin.ts
97
+ var init_plugin = __esm({
98
+ "src/shared/api/plugin.ts"() {
99
+ "use strict";
100
+ }
101
+ });
102
+
103
+ // src/shared/types/console.ts
104
+ var init_console = __esm({
105
+ "src/shared/types/console.ts"() {
106
+ "use strict";
107
+ }
108
+ });
109
+
110
+ // src/shared/types/ipc.ts
111
+ var init_ipc = __esm({
112
+ "src/shared/types/ipc.ts"() {
113
+ "use strict";
114
+ }
115
+ });
116
+
9
117
  // src/shared/api/dsl.ts
10
118
  function defineVisualTest(scenario) {
11
119
  return scenario;
@@ -14,10 +122,13 @@ var VIEWPORT_PRESETS;
14
122
  var init_dsl = __esm({
15
123
  "src/shared/api/dsl.ts"() {
16
124
  "use strict";
125
+ init_plugin();
126
+ init_console();
127
+ init_ipc();
17
128
  VIEWPORT_PRESETS = {
18
- /** Minimum supported window size (e.g., ) */
129
+ /** Minimum supported window size */
19
130
  MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
20
- /** Standard default window size (e.g., ) */
131
+ /** Standard default window size */
21
132
  DEFAULT: { name: "default", width: 1200, height: 800 },
22
133
  /** Wide screen for checking grids and tables */
23
134
  WIDE: { name: "wide", width: 1600, height: 900 },
@@ -28,8 +139,8 @@ var init_dsl = __esm({
28
139
  });
29
140
 
30
141
  // src/features/capture/capture.ts
31
- import fs from "fs";
32
- import path from "path";
142
+ import fs2 from "fs";
143
+ import path2 from "path";
33
144
  var CaptureEngine;
34
145
  var init_capture = __esm({
35
146
  "src/features/capture/capture.ts"() {
@@ -40,8 +151,8 @@ var init_capture = __esm({
40
151
  recordedSnapshots = [];
41
152
  constructor(outputDir) {
42
153
  this.outputDir = outputDir;
43
- if (!fs.existsSync(this.outputDir)) {
44
- fs.mkdirSync(this.outputDir, { recursive: true });
154
+ if (!fs2.existsSync(this.outputDir)) {
155
+ fs2.mkdirSync(this.outputDir, { recursive: true });
45
156
  }
46
157
  }
47
158
  getSnapshots() {
@@ -57,7 +168,7 @@ var init_capture = __esm({
57
168
  const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
58
169
  const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
59
170
  const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
60
- const filePath = path.join(this.outputDir, fileName);
171
+ const filePath = path2.join(this.outputDir, fileName);
61
172
  if (options?.selector) {
62
173
  const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
63
174
  await element.screenshot({ path: filePath });
@@ -93,7 +204,7 @@ var init_capture = __esm({
93
204
  const elapsedMs = frame * interval;
94
205
  const paddedFrame = String(frame + 1).padStart(2, "0");
95
206
  const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
96
- const filePath = path.join(this.outputDir, fileName);
207
+ const filePath = path2.join(this.outputDir, fileName);
97
208
  if (options.selector) {
98
209
  const element = await page.$(options.selector);
99
210
  if (element) {
@@ -130,8 +241,8 @@ var init_capture = __esm({
130
241
  });
131
242
 
132
243
  // src/features/reporter/reporter.ts
133
- import fs2 from "fs";
134
- import path2 from "path";
244
+ import fs3 from "fs";
245
+ import path3 from "path";
135
246
  var VisualReporter;
136
247
  var init_reporter = __esm({
137
248
  "src/features/reporter/reporter.ts"() {
@@ -147,8 +258,8 @@ var init_reporter = __esm({
147
258
  targetMode,
148
259
  durationMs
149
260
  } = data;
150
- const manifestPath = path2.join(outputDir, "manifest.json");
151
- const reportPath = path2.join(outputDir, "report.md");
261
+ const manifestPath = path3.join(outputDir, "manifest.json");
262
+ const reportPath = path3.join(outputDir, "report.md");
152
263
  const manifest = {
153
264
  scenarioId: scenario.id,
154
265
  title: scenario.title,
@@ -163,7 +274,7 @@ var init_reporter = __esm({
163
274
  errors: consoleErrors,
164
275
  warnings: consoleWarnings
165
276
  };
166
- fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
277
+ fs3.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
167
278
  const rows = [];
168
279
  const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
169
280
  const burstGroups = /* @__PURE__ */ new Map();
@@ -256,6 +367,17 @@ ${err.stack}
256
367
  if (consoleWarnings.length > 20) {
257
368
  consoleSections += `
258
369
  *...and ${consoleWarnings.length - 20} more warnings (full list in manifest.json)*
370
+ `;
371
+ }
372
+ }
373
+ let pluginSections = "";
374
+ if (data.customSections && data.customSections.length > 0) {
375
+ for (const sec of data.customSections) {
376
+ pluginSections += `
377
+ ## ${sec.title}
378
+
379
+ ${sec.content}
380
+
259
381
  `;
260
382
  }
261
383
  }
@@ -278,6 +400,7 @@ ${rows.join("\n")}
278
400
 
279
401
  ${burstSections}
280
402
  ${consoleSections}
403
+ ${pluginSections}
281
404
  ---
282
405
 
283
406
  ## \u{1F4CB} AI Agent Verification Checklist:
@@ -291,7 +414,7 @@ ${consoleSections}
291
414
  - [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
292
415
 
293
416
  `;
294
- fs2.writeFileSync(reportPath, reportContent, "utf-8");
417
+ fs3.writeFileSync(reportPath, reportContent, "utf-8");
295
418
  return reportPath;
296
419
  }
297
420
  static getHealthStatus(errors, warnings) {
@@ -429,551 +552,154 @@ var init_playwrightLoader = __esm({
429
552
  }
430
553
  });
431
554
 
432
- // src/shared/drivers/desktopDriver.ts
433
- import { spawn } from "child_process";
555
+ // src/shared/drivers/previewDriver.ts
434
556
  import http from "http";
435
- import path3 from "path";
436
- import fs3 from "fs";
437
- import treeKill from "tree-kill";
438
- var DesktopDriver;
439
- var init_desktopDriver = __esm({
440
- "src/shared/drivers/desktopDriver.ts"() {
557
+ import fs4 from "fs";
558
+ import path4 from "path";
559
+ var MIME_TYPES, PreviewDriver;
560
+ var init_previewDriver = __esm({
561
+ "src/shared/drivers/previewDriver.ts"() {
441
562
  "use strict";
442
563
  init_playwrightLoader();
443
- DesktopDriver = class {
444
- port;
445
- executablePath;
446
- autoLaunch;
447
- args;
448
- env;
449
- cwd;
450
- childProcess = null;
564
+ init_config();
565
+ MIME_TYPES = {
566
+ ".html": "text/html; charset=utf-8",
567
+ ".js": "application/javascript; charset=utf-8",
568
+ ".css": "text/css; charset=utf-8",
569
+ ".json": "application/json; charset=utf-8",
570
+ ".png": "image/png",
571
+ ".jpg": "image/jpeg",
572
+ ".jpeg": "image/jpeg",
573
+ ".gif": "image/gif",
574
+ ".svg": "image/svg+xml",
575
+ ".ico": "image/x-icon",
576
+ ".woff": "font/woff",
577
+ ".woff2": "font/woff2",
578
+ ".ttf": "font/ttf"
579
+ };
580
+ PreviewDriver = class {
581
+ server = null;
451
582
  browser = null;
452
583
  context = null;
453
584
  page = null;
454
- processStderr = "";
455
- processExited = false;
456
- exitCode = null;
585
+ serverPort = 0;
586
+ options;
587
+ _baseUrl = "";
588
+ initialRouteMocks = [];
457
589
  constructor(options) {
458
- this.port = options?.port || 9222;
459
- this.autoLaunch = options?.autoLaunch ?? true;
460
- this.executablePath = options?.executablePath;
461
- this.args = options?.args || [];
462
- this.env = options?.env || {};
463
- this.cwd = options?.cwd;
464
- }
465
- async isPortAvailable() {
466
- return new Promise((resolve) => {
467
- const req = http.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
468
- resolve(res.statusCode === 200);
469
- });
470
- req.on("error", () => resolve(false));
471
- req.setTimeout(800, () => {
472
- req.destroy();
473
- resolve(false);
474
- });
475
- });
476
- }
477
- async waitForPort(timeoutMs = 25e3) {
478
- const startTime = Date.now();
479
- while (Date.now() - startTime < timeoutMs) {
480
- if (this.processExited) {
481
- throw new Error(
482
- `[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
483
- Stderr: ${this.processStderr.trim() || "(none)"}`
484
- );
485
- }
486
- if (await this.isPortAvailable()) {
487
- return;
488
- }
489
- await new Promise((r) => setTimeout(r, 400));
590
+ this.options = options || {};
591
+ if (!this.options.url) {
592
+ this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
490
593
  }
491
- throw new Error(
492
- `Timeout waiting for WebView2/Chromium CDP port ${this.port}. Last stderr:
493
- ${this.processStderr.trim() || "(no stderr output)"}`
494
- );
594
+ }
595
+ get baseUrl() {
596
+ return this._baseUrl;
495
597
  }
496
598
  async start(initialViewport = { width: 1200, height: 800 }) {
497
- const alreadyRunning = await this.isPortAvailable();
498
- if (!alreadyRunning) {
499
- if (!this.autoLaunch) {
500
- throw new Error(
501
- `App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
502
- );
503
- }
504
- if (!this.executablePath) {
505
- throw new Error(
506
- `[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
507
- );
508
- }
509
- const resolvedExe = path3.resolve(process.cwd(), this.executablePath);
510
- if (!fs3.existsSync(resolvedExe)) {
511
- throw new Error(
512
- `[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
513
- );
514
- }
515
- console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
516
- const finalArgs = [...this.args];
517
- const hasDebugPort = finalArgs.some((a) => a.startsWith("--remote-debugging-port="));
518
- if (!hasDebugPort) {
519
- finalArgs.push(`--remote-debugging-port=${this.port}`);
599
+ let targetUrl = this.options.url;
600
+ if (!targetUrl) {
601
+ if (!fs4.existsSync(this.options.wwwrootDir)) {
602
+ throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
520
603
  }
521
- const mergedEnv = {
522
- ...process.env,
523
- ...this.env,
524
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${this.port}`
525
- };
526
- this.processStderr = "";
527
- this.processExited = false;
528
- this.exitCode = null;
529
- this.childProcess = spawn(resolvedExe, finalArgs, {
530
- env: mergedEnv,
531
- cwd: this.cwd || path3.dirname(resolvedExe),
532
- stdio: ["ignore", "ignore", "pipe"],
533
- detached: false
534
- });
535
- ;
536
- this.processStderr = "";
537
- this.processExited = false;
538
- this.exitCode = null;
539
- this.childProcess = spawn(resolvedExe, this.args, {
540
- env: mergedEnv,
541
- cwd: this.cwd || path3.dirname(resolvedExe),
542
- stdio: ["ignore", "ignore", "pipe"],
543
- detached: false
544
- });
545
- this.childProcess.stderr?.on("data", (chunk) => {
546
- this.processStderr += chunk.toString();
547
- });
548
- this.childProcess.on("exit", (code) => {
549
- this.processExited = true;
550
- this.exitCode = code;
551
- });
552
- this.childProcess.on("error", (err) => {
553
- console.error("[DesktopDriver] Failed to spawn process:", err);
604
+ await new Promise((resolve, reject) => {
605
+ this.server = http.createServer((req, res) => {
606
+ let reqUrl = req.url?.split("?")[0] || "/";
607
+ if (reqUrl === "/") reqUrl = "/index.html";
608
+ let safePath = path4.normalize(path4.join(this.options.wwwrootDir, reqUrl));
609
+ if (!safePath.startsWith(this.options.wwwrootDir)) {
610
+ res.writeHead(403);
611
+ return res.end("Forbidden");
612
+ }
613
+ if (!fs4.existsSync(safePath) || fs4.statSync(safePath).isDirectory()) {
614
+ safePath = path4.join(this.options.wwwrootDir, "index.html");
615
+ }
616
+ const ext = path4.extname(safePath).toLowerCase();
617
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
618
+ try {
619
+ const content = fs4.readFileSync(safePath);
620
+ res.writeHead(200, { "Content-Type": contentType });
621
+ res.end(content);
622
+ } catch (e) {
623
+ res.writeHead(500);
624
+ res.end(`Server error: ${e.message}`);
625
+ }
626
+ });
627
+ this.server.listen(0, "127.0.0.1", () => {
628
+ const addr = this.server?.address();
629
+ if (typeof addr === "object" && addr?.port) {
630
+ this.serverPort = addr.port;
631
+ this._baseUrl = `http://127.0.0.1:${this.serverPort}`;
632
+ resolve();
633
+ } else {
634
+ reject(new Error("Failed to acquire port for static preview server"));
635
+ }
636
+ });
554
637
  });
555
- console.log(`[DesktopDriver] Waiting for CDP debugging port on ${this.port}...`);
556
- await this.waitForPort();
638
+ targetUrl = `${this._baseUrl}/index.html`;
557
639
  } else {
558
- console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
640
+ this._baseUrl = targetUrl;
641
+ console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
559
642
  }
560
- console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
561
- this.browser = await chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
562
- const contexts = this.browser.contexts();
563
- this.context = contexts[0] || await this.browser.newContext();
564
- const pages = this.context.pages();
565
- if (pages.length > 0) {
566
- this.page = pages[0];
567
- } else {
568
- this.page = await this.context.waitForEvent("page", { timeout: 1e4 });
643
+ this.browser = await chromium.launch({
644
+ headless: !this.options.headed,
645
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
646
+ });
647
+ this.context = await this.browser.newContext({
648
+ viewport: initialViewport,
649
+ deviceScaleFactor: 1
650
+ });
651
+ this.page = await this.context.newPage();
652
+ if (this.initialRouteMocks.length > 0) {
653
+ for (const entry of this.initialRouteMocks) {
654
+ await this.addRouteMock(entry);
655
+ }
656
+ }
657
+ await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
658
+ return { page: this.page, context: this.context, browser: this.browser };
659
+ }
660
+ async addRouteMock(entry) {
661
+ if (!this.page) {
662
+ this.initialRouteMocks.push(entry);
663
+ return;
569
664
  }
570
- if (initialViewport) {
571
- await this.page.setViewportSize(initialViewport).catch(() => {
665
+ await this.page.route(entry.url, async (route) => {
666
+ const req = route.request();
667
+ if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
668
+ return route.continue();
669
+ }
670
+ if (entry.delayMs) {
671
+ await new Promise((r) => setTimeout(r, entry.delayMs));
672
+ }
673
+ const isJson = typeof entry.body === "object" && entry.body !== null;
674
+ await route.fulfill({
675
+ status: entry.status ?? 200,
676
+ contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
677
+ body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
678
+ headers: entry.headers
572
679
  });
680
+ });
681
+ }
682
+ async setupRouteMocks(routes) {
683
+ for (const r of routes) {
684
+ await this.addRouteMock(r);
573
685
  }
574
- return { page: this.page, context: this.context, browser: this.browser };
575
686
  }
576
687
  async stop() {
688
+ if (this.context) {
689
+ await this.context.close().catch(() => {
690
+ });
691
+ this.context = null;
692
+ }
577
693
  if (this.browser) {
578
694
  await this.browser.close().catch(() => {
579
695
  });
580
696
  this.browser = null;
581
697
  }
582
- if (this.childProcess && !this.childProcess.killed && this.childProcess.pid) {
583
- console.log("[DesktopDriver] Terminating spawned desktop process tree...");
584
- const pid = this.childProcess.pid;
698
+ if (this.server) {
585
699
  await new Promise((resolve) => {
586
- treeKill(pid, "SIGTERM", () => resolve());
700
+ this.server?.close(() => resolve());
587
701
  });
588
- this.childProcess = null;
589
- }
590
- }
591
- };
592
- }
593
- });
594
-
595
- // src/features/mock-ipc/mockIpc.ts
596
- function generateMockIpcScript(registry) {
597
- const mocksJson = JSON.stringify(registry.toSerializable());
598
- return `
599
- (() => {
600
- const __mockTable = ${mocksJson};
601
-
602
- window.__visualRunnerMocks = __mockTable;
603
-
604
- // Generic IPC mock bridge for modern web applications
605
- window.__mockIpc = {
606
- invoke: (action, payload) => {
607
- return new Promise((resolve, reject) => {
608
- const mock = window.__visualRunnerMocks[action];
609
-
610
- if (mock) {
611
- setTimeout(() => {
612
- if (mock.type === 'ERROR') {
613
- reject(new Error(mock.data));
614
- } else {
615
- resolve(mock.data);
616
- }
617
- }, mock.delayMs || 20);
618
- } else {
619
- console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
620
- setTimeout(() => resolve(null), 20);
621
- }
622
- });
623
- }
624
- };
625
-
626
- // Safe fallback bridge for hybrid webviews (Photino / CEF / WebView2)
627
- try {
628
- if (!window.external) {
629
- (window as any).external = {};
630
- }
631
- (window.external as any).sendMessage = (msg) => {
632
- try {
633
- const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg;
634
- const action = parsed.Action || parsed.action;
635
- const id = parsed.Id || parsed.id;
636
- const mock = (window as any).__visualRunnerMocks[action];
637
-
638
- if (mock) {
639
- const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
640
- setTimeout(() => {
641
- const cb = (window as any).__mockCallback;
642
- if (typeof cb === 'function') cb(JSON.stringify(response));
643
- }, mock.delayMs || 20);
644
- } else {
645
- setTimeout(() => {
646
- const cb = (window as any).__mockCallback;
647
- if (typeof cb === 'function') cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
648
- }, 20);
649
- }
650
- } catch (e) {
651
- console.error('[Mock IPC] Failed to process message:', e);
652
- }
653
- };
654
-
655
- (window.external as any).receiveMessage = (callback) => {
656
- (window as any).__mockCallback = callback;
657
- };
658
- } catch {
659
- // Ignored if window.external is read-only in strict Chromium sandboxes
660
- }
661
-
662
- console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys((window as any).__visualRunnerMocks).length, 'mocked actions');
663
- })();
664
-
665
- `;
666
- }
667
- var MockIpcRegistry;
668
- var init_mockIpc = __esm({
669
- "src/features/mock-ipc/mockIpc.ts"() {
670
- "use strict";
671
- MockIpcRegistry = class {
672
- mocks = /* @__PURE__ */ new Map();
673
- set(action, data, options) {
674
- this.mocks.set(action, {
675
- action,
676
- data,
677
- type: options?.type ?? "SUCCESS",
678
- delayMs: options?.delayMs ?? 20
679
- });
680
- }
681
- setBatch(entries) {
682
- for (const entry of entries) {
683
- this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
684
- }
685
- }
686
- remove(action) {
687
- this.mocks.delete(action);
688
- }
689
- clear() {
690
- this.mocks.clear();
691
- }
692
- get(action) {
693
- return this.mocks.get(action) ?? null;
694
- }
695
- toSerializable() {
696
- const result = {};
697
- for (const [action, entry] of this.mocks.entries()) {
698
- result[action] = {
699
- data: entry.data,
700
- type: entry.type ?? "SUCCESS",
701
- delayMs: entry.delayMs ?? 20
702
- };
703
- }
704
- return result;
705
- }
706
- get size() {
707
- return this.mocks.size;
708
- }
709
- };
710
- }
711
- });
712
-
713
- // src/shared/lib/config.ts
714
- import path4 from "path";
715
- import fs4 from "fs";
716
- function loadConfig(cwd = process.cwd()) {
717
- const jsonConfigPath = path4.join(cwd, "agent-lens.json");
718
- if (fs4.existsSync(jsonConfigPath)) {
719
- try {
720
- const raw = fs4.readFileSync(jsonConfigPath, "utf-8");
721
- return JSON.parse(raw);
722
- } catch (e) {
723
- console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
724
- }
725
- }
726
- const packageJsonPath = path4.join(cwd, "package.json");
727
- if (fs4.existsSync(packageJsonPath)) {
728
- try {
729
- const raw = fs4.readFileSync(packageJsonPath, "utf-8");
730
- const pkg = JSON.parse(raw);
731
- if (pkg.agentLens && typeof pkg.agentLens === "object") {
732
- return pkg.agentLens;
733
- }
734
- } catch {
735
- }
736
- }
737
- return {};
738
- }
739
- function detectStartCwd(providedCwd) {
740
- if (providedCwd) {
741
- const resolved = path4.resolve(process.cwd(), providedCwd);
742
- if (fs4.existsSync(resolved)) {
743
- return providedCwd;
744
- }
745
- }
746
- const rootPkgPath = path4.join(process.cwd(), "package.json");
747
- let rootHasDevScript = false;
748
- if (fs4.existsSync(rootPkgPath)) {
749
- try {
750
- const rootPkg = JSON.parse(fs4.readFileSync(rootPkgPath, "utf-8"));
751
- rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
752
- } catch {
753
- }
754
- }
755
- if (rootHasDevScript) {
756
- return void 0;
757
- }
758
- const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
759
- for (const candidate of candidates) {
760
- const candidatePkg = path4.join(process.cwd(), candidate, "package.json");
761
- if (fs4.existsSync(candidatePkg)) {
762
- return `./${candidate}`;
763
- }
764
- }
765
- return void 0;
766
- }
767
- function resolveWwwrootDir(customDir) {
768
- if (customDir) {
769
- return path4.resolve(process.cwd(), customDir);
770
- }
771
- const candidates = [
772
- "dist",
773
- "build",
774
- "out",
775
- "wwwroot",
776
- "Frontend/dist",
777
- "frontend/dist",
778
- "client/dist"
779
- ];
780
- for (const c of candidates) {
781
- const candidatePath = path4.resolve(process.cwd(), c);
782
- if (fs4.existsSync(candidatePath) && fs4.existsSync(path4.join(candidatePath, "index.html"))) {
783
- return candidatePath;
784
- }
785
- }
786
- for (const c of candidates) {
787
- const candidatePath = path4.resolve(process.cwd(), c);
788
- if (fs4.existsSync(candidatePath)) {
789
- return candidatePath;
790
- }
791
- }
792
- return path4.resolve(process.cwd(), "dist");
793
- }
794
- var init_config = __esm({
795
- "src/shared/lib/config.ts"() {
796
- "use strict";
797
- }
798
- });
799
-
800
- // src/shared/drivers/previewDriver.ts
801
- import http2 from "http";
802
- import fs5 from "fs";
803
- import path5 from "path";
804
- var MIME_TYPES, PreviewDriver;
805
- var init_previewDriver = __esm({
806
- "src/shared/drivers/previewDriver.ts"() {
807
- "use strict";
808
- init_playwrightLoader();
809
- init_mockIpc();
810
- init_config();
811
- MIME_TYPES = {
812
- ".html": "text/html; charset=utf-8",
813
- ".js": "application/javascript; charset=utf-8",
814
- ".css": "text/css; charset=utf-8",
815
- ".json": "application/json; charset=utf-8",
816
- ".png": "image/png",
817
- ".jpg": "image/jpeg",
818
- ".jpeg": "image/jpeg",
819
- ".gif": "image/gif",
820
- ".svg": "image/svg+xml",
821
- ".ico": "image/x-icon",
822
- ".woff": "font/woff",
823
- ".woff2": "font/woff2",
824
- ".ttf": "font/ttf"
825
- };
826
- PreviewDriver = class {
827
- server = null;
828
- browser = null;
829
- context = null;
830
- page = null;
831
- serverPort = 0;
832
- options;
833
- _baseUrl = "";
834
- initialRouteMocks = [];
835
- mockRegistry;
836
- constructor(options) {
837
- this.options = options || {};
838
- if (!this.options.url) {
839
- this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
840
- }
841
- this.mockRegistry = new MockIpcRegistry();
842
- }
843
- get baseUrl() {
844
- return this._baseUrl;
845
- }
846
- async start(initialViewport = { width: 1200, height: 800 }) {
847
- let targetUrl = this.options.url;
848
- if (!targetUrl) {
849
- if (!fs5.existsSync(this.options.wwwrootDir)) {
850
- throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
851
- }
852
- await new Promise((resolve, reject) => {
853
- this.server = http2.createServer((req, res) => {
854
- let reqUrl = req.url?.split("?")[0] || "/";
855
- if (reqUrl === "/") reqUrl = "/index.html";
856
- let safePath = path5.normalize(path5.join(this.options.wwwrootDir, reqUrl));
857
- if (!safePath.startsWith(this.options.wwwrootDir)) {
858
- res.writeHead(403);
859
- return res.end("Forbidden");
860
- }
861
- if (!fs5.existsSync(safePath) || fs5.statSync(safePath).isDirectory()) {
862
- safePath = path5.join(this.options.wwwrootDir, "index.html");
863
- }
864
- const ext = path5.extname(safePath).toLowerCase();
865
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
866
- try {
867
- const content = fs5.readFileSync(safePath);
868
- res.writeHead(200, { "Content-Type": contentType });
869
- res.end(content);
870
- } catch (e) {
871
- res.writeHead(500);
872
- res.end(`Server error: ${e.message}`);
873
- }
874
- });
875
- this.server.listen(0, "127.0.0.1", () => {
876
- const addr = this.server?.address();
877
- if (typeof addr === "object" && addr?.port) {
878
- this.serverPort = addr.port;
879
- this._baseUrl = `http://127.0.0.1:${this.serverPort}`;
880
- resolve();
881
- } else {
882
- reject(new Error("Failed to acquire port for static preview server"));
883
- }
884
- });
885
- });
886
- targetUrl = `${this._baseUrl}/index.html`;
887
- } else {
888
- this._baseUrl = targetUrl;
889
- console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
890
- }
891
- this.browser = await chromium.launch({
892
- headless: !this.options.headed,
893
- args: ["--no-sandbox", "--disable-setuid-sandbox"]
894
- });
895
- this.context = await this.browser.newContext({
896
- viewport: initialViewport,
897
- deviceScaleFactor: 1
898
- });
899
- if (this.mockRegistry.size > 0) {
900
- const mockScript = generateMockIpcScript(this.mockRegistry);
901
- await this.context.addInitScript(mockScript);
902
- }
903
- this.page = await this.context.newPage();
904
- if (this.initialRouteMocks.length > 0) {
905
- for (const entry of this.initialRouteMocks) {
906
- await this.addRouteMock(entry);
907
- }
908
- }
909
- await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
910
- return { page: this.page, context: this.context, browser: this.browser };
911
- }
912
- async addRouteMock(entry) {
913
- if (!this.page) {
914
- this.initialRouteMocks.push(entry);
915
- return;
916
- }
917
- await this.page.route(entry.url, async (route) => {
918
- const req = route.request();
919
- if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
920
- return route.continue();
921
- }
922
- if (entry.delayMs) {
923
- await new Promise((r) => setTimeout(r, entry.delayMs));
924
- }
925
- const isJson = typeof entry.body === "object" && entry.body !== null;
926
- await route.fulfill({
927
- status: entry.status ?? 200,
928
- contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
929
- body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
930
- headers: entry.headers
931
- });
932
- });
933
- }
934
- async setupRouteMocks(routes) {
935
- for (const r of routes) {
936
- await this.addRouteMock(r);
937
- }
938
- }
939
- async updateMockIpc(action, data, options) {
940
- if (!this.page) {
941
- throw new Error("PreviewDriver not started. Call start() first.");
942
- }
943
- this.mockRegistry.set(action, data, options);
944
- await this.page.evaluate(
945
- ({ action: action2, mock }) => {
946
- if (!window.__visualRunnerMocks) {
947
- window.__visualRunnerMocks = {};
948
- }
949
- window.__visualRunnerMocks[action2] = mock;
950
- },
951
- {
952
- action,
953
- mock: {
954
- data,
955
- type: options?.type ?? "SUCCESS",
956
- delayMs: options?.delayMs ?? 20
957
- }
958
- }
959
- );
960
- }
961
- async stop() {
962
- if (this.context) {
963
- await this.context.close().catch(() => {
964
- });
965
- this.context = null;
966
- }
967
- if (this.browser) {
968
- await this.browser.close().catch(() => {
969
- });
970
- this.browser = null;
971
- }
972
- if (this.server) {
973
- await new Promise((resolve) => {
974
- this.server?.close(() => resolve());
975
- });
976
- this.server = null;
702
+ this.server = null;
977
703
  }
978
704
  }
979
705
  };
@@ -981,8 +707,8 @@ var init_previewDriver = __esm({
981
707
  });
982
708
 
983
709
  // src/shared/lib/processManager.ts
984
- import { spawn as spawn2 } from "child_process";
985
- import treeKill2 from "tree-kill";
710
+ import { spawn } from "child_process";
711
+ import treeKill from "tree-kill";
986
712
  var ProcessManager;
987
713
  var init_processManager = __esm({
988
714
  "src/shared/lib/processManager.ts"() {
@@ -993,7 +719,7 @@ var init_processManager = __esm({
993
719
  async start(command, options) {
994
720
  const cwd = options?.cwd || process.cwd();
995
721
  console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
996
- this.childProcess = spawn2(command, {
722
+ this.childProcess = spawn(command, {
997
723
  cwd,
998
724
  env: { ...process.env, ...options?.env },
999
725
  shell: true,
@@ -1044,10 +770,10 @@ var init_processManager = __esm({
1044
770
  const pid = this.childProcess.pid;
1045
771
  console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
1046
772
  await new Promise((resolve) => {
1047
- treeKill2(pid, "SIGTERM", (err) => {
773
+ treeKill(pid, "SIGTERM", (err) => {
1048
774
  if (err) {
1049
775
  try {
1050
- treeKill2(pid, "SIGKILL");
776
+ treeKill(pid, "SIGKILL");
1051
777
  } catch {
1052
778
  }
1053
779
  }
@@ -1056,37 +782,465 @@ var init_processManager = __esm({
1056
782
  });
1057
783
  this.childProcess = null;
1058
784
  }
1059
- };
785
+ };
786
+ }
787
+ });
788
+
789
+ // src/shared/lib/pluginLoader.ts
790
+ import path5 from "path";
791
+ import fs5 from "fs";
792
+ import { createJiti } from "jiti";
793
+ function findPackageRoot() {
794
+ let cur = __dirname;
795
+ while (cur !== path5.dirname(cur)) {
796
+ const pkgPath = path5.join(cur, "package.json");
797
+ if (fs5.existsSync(pkgPath)) {
798
+ try {
799
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
800
+ if (pkg.name === "@_deep4wee/agent-lens" || pkg.name === "agent-lens") {
801
+ return cur;
802
+ }
803
+ } catch {
804
+ }
805
+ }
806
+ cur = path5.dirname(cur);
807
+ }
808
+ return path5.resolve(__dirname, "..");
809
+ }
810
+ var PluginManager;
811
+ var init_pluginLoader = __esm({
812
+ "src/shared/lib/pluginLoader.ts"() {
813
+ "use strict";
814
+ PluginManager = class {
815
+ plugins = [];
816
+ jiti = createJiti(process.cwd());
817
+ register(plugin) {
818
+ if (this.plugins.some((p) => p.name === plugin.name)) {
819
+ return;
820
+ }
821
+ this.plugins.push(plugin);
822
+ console.log(`\u{1F50C} [Plugin] Registered: ${plugin.name}${plugin.version ? ` (v${plugin.version})` : ""}`);
823
+ }
824
+ async load(pluginSpec) {
825
+ if (typeof pluginSpec === "object" && pluginSpec !== null) {
826
+ this.register(pluginSpec);
827
+ return;
828
+ }
829
+ const pluginNameOrPath = pluginSpec.trim();
830
+ if (!pluginNameOrPath) return;
831
+ if (pluginNameOrPath.startsWith(".") || pluginNameOrPath.startsWith("/") || pluginNameOrPath.includes("/") || pluginNameOrPath.includes("\\")) {
832
+ const resolvedPath = path5.resolve(process.cwd(), pluginNameOrPath);
833
+ await this.loadFromFile(resolvedPath);
834
+ return;
835
+ }
836
+ const localCandidates = [
837
+ path5.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.ts`),
838
+ path5.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.js`),
839
+ path5.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.ts`),
840
+ path5.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.js`),
841
+ path5.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.ts"),
842
+ path5.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.js"),
843
+ path5.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.ts"),
844
+ path5.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.js")
845
+ ];
846
+ for (const candidate of localCandidates) {
847
+ if (fs5.existsSync(candidate)) {
848
+ await this.loadFromFile(candidate);
849
+ return;
850
+ }
851
+ }
852
+ const pkgRoot = findPackageRoot();
853
+ const builtInCandidates = [
854
+ path5.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.ts"),
855
+ path5.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.js"),
856
+ path5.join(pkgRoot, "dist", "plugins", pluginNameOrPath, "index.js"),
857
+ path5.join(pkgRoot, "plugins", pluginNameOrPath, "index.ts"),
858
+ path5.join(pkgRoot, "plugins", pluginNameOrPath, "index.js"),
859
+ path5.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.ts"),
860
+ path5.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.js"),
861
+ path5.resolve(__dirname, "../plugins", pluginNameOrPath, "index.ts"),
862
+ path5.resolve(__dirname, "../plugins", pluginNameOrPath, "index.js")
863
+ ];
864
+ for (const builtIn of builtInCandidates) {
865
+ if (fs5.existsSync(builtIn)) {
866
+ await this.loadFromFile(builtIn);
867
+ return;
868
+ }
869
+ }
870
+ try {
871
+ const mod = await this.jiti.import(pluginNameOrPath);
872
+ const plugin = mod.default || mod.plugin || mod;
873
+ if (plugin && plugin.name) {
874
+ this.register(plugin);
875
+ } else {
876
+ console.warn(`\u26A0\uFE0F [Plugin] Package "${pluginNameOrPath}" did not export a valid AgentLensPlugin.`);
877
+ }
878
+ } catch (err) {
879
+ console.warn(`\u26A0\uFE0F [Plugin] Could not load plugin '${pluginNameOrPath}': ${err.message}`);
880
+ }
881
+ }
882
+ async loadFromFile(filePath) {
883
+ if (!fs5.existsSync(filePath)) {
884
+ console.warn(`\u26A0\uFE0F [Plugin] Plugin file not found: ${filePath}`);
885
+ return;
886
+ }
887
+ try {
888
+ const mod = await this.jiti.import(filePath);
889
+ const plugin = mod.default || mod.plugin || mod;
890
+ if (plugin && plugin.name) {
891
+ this.register(plugin);
892
+ } else {
893
+ console.warn(`\u26A0\uFE0F [Plugin] File "${filePath}" does not export a valid AgentLensPlugin by default.`);
894
+ }
895
+ } catch (err) {
896
+ console.error(`\u274C [Plugin] Failed to import plugin from ${filePath}:`, err.message);
897
+ }
898
+ }
899
+ async loadAll(specs) {
900
+ for (const spec of specs) {
901
+ await this.load(spec);
902
+ }
903
+ }
904
+ async runSetup(hookContext) {
905
+ for (const p of this.plugins) {
906
+ if (p.setup) await p.setup(hookContext);
907
+ }
908
+ }
909
+ async launchSession(options, hookContext) {
910
+ for (const p of this.plugins) {
911
+ if (p.launchSession) {
912
+ const session = await p.launchSession(options, hookContext);
913
+ if (session) {
914
+ return session;
915
+ }
916
+ }
917
+ }
918
+ return void 0;
919
+ }
920
+ async runOnContextCreated(context, hookContext) {
921
+ for (const p of this.plugins) {
922
+ if (p.onContextCreated) await p.onContextCreated(context, hookContext);
923
+ }
924
+ }
925
+ async runOnPageCreated(page, context, hookContext) {
926
+ for (const p of this.plugins) {
927
+ if (p.onPageCreated) await p.onPageCreated(page, context, hookContext);
928
+ }
929
+ }
930
+ async extendContext(ctx, page, hookContext) {
931
+ for (const p of this.plugins) {
932
+ if (p.extendContext) {
933
+ const extensions = await p.extendContext(ctx, page, hookContext);
934
+ if (extensions && typeof extensions === "object") {
935
+ Object.assign(ctx, extensions);
936
+ }
937
+ }
938
+ }
939
+ }
940
+ async runOnAfterRun(reportData, hookContext) {
941
+ for (const p of this.plugins) {
942
+ if (p.onAfterRun) await p.onAfterRun(reportData, hookContext);
943
+ }
944
+ }
945
+ async runTeardown(hookContext) {
946
+ for (const p of this.plugins) {
947
+ if (p.teardown) {
948
+ try {
949
+ await p.teardown(hookContext);
950
+ } catch (e) {
951
+ console.error(`\u26A0\uFE0F [Plugin] Error in ${p.name}.teardown:`, e.message);
952
+ }
953
+ }
954
+ }
955
+ }
956
+ get loadedPlugins() {
957
+ return [...this.plugins];
958
+ }
959
+ };
960
+ }
961
+ });
962
+
963
+ // src/features/runner/lib/navigation.ts
964
+ function createNavigator(page, baseUrl, targetUrl) {
965
+ return async (route) => {
966
+ if (route.startsWith("http://") || route.startsWith("https://")) {
967
+ await page.goto(route, { waitUntil: "domcontentloaded" });
968
+ await page.waitForTimeout(300);
969
+ return;
970
+ }
971
+ if (route.startsWith("#")) {
972
+ await page.evaluate((r) => {
973
+ window.location.hash = r;
974
+ }, route);
975
+ await page.waitForTimeout(300);
976
+ return;
977
+ }
978
+ if (baseUrl && targetUrl) {
979
+ try {
980
+ const fullUrl = new URL(route, baseUrl).toString();
981
+ await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
982
+ await page.waitForTimeout(300);
983
+ return;
984
+ } catch {
985
+ }
986
+ }
987
+ const routePath = route.startsWith("/") ? route : `/${route}`;
988
+ await page.evaluate((r) => {
989
+ if (window.location.hash !== void 0) {
990
+ window.location.hash = r;
991
+ }
992
+ }, routePath);
993
+ await page.waitForTimeout(300);
994
+ };
995
+ }
996
+ var init_navigation = __esm({
997
+ "src/features/runner/lib/navigation.ts"() {
998
+ "use strict";
999
+ }
1000
+ });
1001
+
1002
+ // src/features/runner/lib/artifactsSync.ts
1003
+ import fs6 from "fs";
1004
+ import path6 from "path";
1005
+ function syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots) {
1006
+ try {
1007
+ const latestDir = path6.join(artifactsRoot, "latest");
1008
+ if (fs6.existsSync(latestDir)) {
1009
+ fs6.rmSync(latestDir, { recursive: true, force: true });
1010
+ }
1011
+ fs6.mkdirSync(latestDir, { recursive: true });
1012
+ if (fs6.existsSync(reportPath)) {
1013
+ fs6.copyFileSync(reportPath, path6.join(latestDir, "report.md"));
1014
+ }
1015
+ const manifestSrc = path6.join(scenarioArtifactsDir, "manifest.json");
1016
+ if (fs6.existsSync(manifestSrc)) {
1017
+ fs6.copyFileSync(manifestSrc, path6.join(latestDir, "manifest.json"));
1018
+ }
1019
+ for (const snap of snapshots) {
1020
+ if (snap.filePath && fs6.existsSync(snap.filePath)) {
1021
+ fs6.copyFileSync(snap.filePath, path6.join(latestDir, snap.fileName));
1022
+ }
1023
+ }
1024
+ return path6.join(latestDir, "report.md");
1025
+ } catch {
1026
+ return void 0;
1027
+ }
1028
+ }
1029
+ var init_artifactsSync = __esm({
1030
+ "src/features/runner/lib/artifactsSync.ts"() {
1031
+ "use strict";
1032
+ }
1033
+ });
1034
+
1035
+ // src/features/runner/lib/contextBuilder.ts
1036
+ function buildTestContext(options) {
1037
+ const {
1038
+ page,
1039
+ context,
1040
+ targetMode,
1041
+ initialViewport,
1042
+ captureEngine,
1043
+ consoleTracker,
1044
+ previewDriver,
1045
+ doNavigate
1046
+ } = options;
1047
+ let currentViewport = { ...initialViewport };
1048
+ const ctx = {
1049
+ page,
1050
+ context,
1051
+ targetMode,
1052
+ currentViewport,
1053
+ // --- Snapshots ---
1054
+ capture: async (name, opts) => {
1055
+ console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
1056
+ return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
1057
+ },
1058
+ captureBurst: async (name, opts) => {
1059
+ console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
1060
+ return await captureEngine.takeBurst(page, name, currentViewport, opts);
1061
+ },
1062
+ // --- Navigation ---
1063
+ navigate: async (route) => {
1064
+ console.log(`\u{1F9ED} [Navigate] ${route}`);
1065
+ await doNavigate(route);
1066
+ },
1067
+ // --- Viewport Management ---
1068
+ resize: async (width, height) => {
1069
+ console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1070
+ currentViewport = { width, height };
1071
+ ctx.currentViewport = currentViewport;
1072
+ await page.setViewportSize(currentViewport);
1073
+ await page.waitForTimeout(200);
1074
+ },
1075
+ setPreset: async (preset) => {
1076
+ console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1077
+ await ctx.resize(preset.width, preset.height);
1078
+ },
1079
+ resizeToFit: async (selector, padding = 0) => {
1080
+ let boundingBox;
1081
+ if (selector) {
1082
+ const el = await page.$(selector);
1083
+ if (el) {
1084
+ boundingBox = await el.boundingBox();
1085
+ }
1086
+ } else {
1087
+ boundingBox = await page.evaluate(() => ({
1088
+ width: document.documentElement.scrollWidth,
1089
+ height: document.documentElement.scrollHeight
1090
+ }));
1091
+ }
1092
+ if (boundingBox) {
1093
+ const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1094
+ const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1095
+ console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1096
+ await ctx.resize(newWidth, newHeight);
1097
+ } else {
1098
+ console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1099
+ }
1100
+ },
1101
+ // --- Waiting & DOM Observation ---
1102
+ wait: async (ms) => {
1103
+ await page.waitForTimeout(ms);
1104
+ },
1105
+ waitForSelector: async (selector, timeoutMs = 5e3) => {
1106
+ await page.waitForSelector(selector, { timeout: timeoutMs });
1107
+ },
1108
+ // --- Interaction ---
1109
+ click: async (selector) => {
1110
+ console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1111
+ await page.click(selector);
1112
+ },
1113
+ rightClick: async (selector) => {
1114
+ console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1115
+ await page.click(selector, { button: "right" });
1116
+ },
1117
+ type: async (selector, text) => {
1118
+ console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1119
+ await page.fill(selector, text);
1120
+ },
1121
+ selectOption: async (selector, value) => {
1122
+ console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1123
+ await page.selectOption(selector, value);
1124
+ },
1125
+ hover: async (selector) => {
1126
+ console.log(`\u{1F446} [Hover] ${selector}`);
1127
+ await page.hover(selector);
1128
+ },
1129
+ scroll: async (selector, deltaY) => {
1130
+ console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1131
+ await page.evaluate(
1132
+ ({ sel, dY }) => {
1133
+ const el = document.querySelector(sel);
1134
+ if (el) {
1135
+ el.scrollTop += dY;
1136
+ } else {
1137
+ window.scrollBy(0, dY);
1138
+ }
1139
+ },
1140
+ { sel: selector, dY: deltaY }
1141
+ );
1142
+ await page.waitForTimeout(100);
1143
+ },
1144
+ // --- Logging ---
1145
+ log: (msg) => {
1146
+ console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1147
+ },
1148
+ // --- Mock IPC (delegated or stubbed if plugin not loaded) ---
1149
+ setMockIpc: async (action, data, mockOptions) => {
1150
+ console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc called for "${action}". Ensure --plugin=mock-ipc is enabled.`);
1151
+ },
1152
+ // --- HTTP Route Mocking ---
1153
+ setMockRoute: async (url, body, routeOptions) => {
1154
+ if (!previewDriver) {
1155
+ console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored (no preview driver running)`);
1156
+ return;
1157
+ }
1158
+ console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
1159
+ await previewDriver.addRouteMock({
1160
+ url,
1161
+ body,
1162
+ method: routeOptions?.method,
1163
+ status: routeOptions?.status,
1164
+ delayMs: routeOptions?.delayMs,
1165
+ headers: routeOptions?.headers
1166
+ });
1167
+ },
1168
+ // --- Console Errors ---
1169
+ getConsoleErrors: () => consoleTracker.getErrors(),
1170
+ getConsoleWarnings: () => consoleTracker.getWarnings(),
1171
+ hasConsoleErrors: () => consoleTracker.hasErrors,
1172
+ // --- DOM Assertions ---
1173
+ readText: async (selector) => {
1174
+ const text = await page.textContent(selector);
1175
+ return text ? text.trim() : null;
1176
+ },
1177
+ getPageText: async () => {
1178
+ return await page.evaluate(() => document.body.innerText || "");
1179
+ },
1180
+ isVisible: async (selector) => {
1181
+ try {
1182
+ const element = await page.$(selector);
1183
+ if (!element) return false;
1184
+ return await element.isVisible();
1185
+ } catch {
1186
+ return false;
1187
+ }
1188
+ },
1189
+ getElementCount: async (selector) => {
1190
+ const elements = await page.$$(selector);
1191
+ return elements.length;
1192
+ }
1193
+ };
1194
+ return {
1195
+ ctx,
1196
+ getCurrentViewport: () => currentViewport
1197
+ };
1198
+ }
1199
+ var init_contextBuilder = __esm({
1200
+ "src/features/runner/lib/contextBuilder.ts"() {
1201
+ "use strict";
1060
1202
  }
1061
1203
  });
1062
1204
 
1063
1205
  // src/features/runner/runner.ts
1064
- import path6 from "path";
1065
- import fs6 from "fs";
1206
+ import path7 from "path";
1207
+ import fs7 from "fs";
1066
1208
  async function runVisualScenario(options) {
1067
1209
  const { scenario } = options;
1068
1210
  const targetMode = options.targetMode || "preview";
1069
1211
  const startTime = Date.now();
1070
1212
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1071
- const artifactsRoot = options.artifactsRoot || path6.resolve(process.cwd(), "artifacts");
1072
- if (options.cleanArtifacts && fs6.existsSync(artifactsRoot)) {
1213
+ const artifactsRoot = options.artifactsRoot || path7.resolve(process.cwd(), "artifacts");
1214
+ if (options.cleanArtifacts && fs7.existsSync(artifactsRoot)) {
1073
1215
  console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
1074
- const entries = fs6.readdirSync(artifactsRoot, { withFileTypes: true });
1216
+ const entries = fs7.readdirSync(artifactsRoot, { withFileTypes: true });
1075
1217
  for (const entry of entries) {
1076
- fs6.rmSync(path6.join(artifactsRoot, entry.name), { recursive: true, force: true });
1218
+ fs7.rmSync(path7.join(artifactsRoot, entry.name), { recursive: true, force: true });
1077
1219
  }
1078
1220
  }
1079
- const scenarioArtifactsDir = path6.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1080
- if (!fs6.existsSync(scenarioArtifactsDir)) {
1081
- fs6.mkdirSync(scenarioArtifactsDir, { recursive: true });
1221
+ const scenarioArtifactsDir = path7.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1222
+ if (!fs7.existsSync(scenarioArtifactsDir)) {
1223
+ fs7.mkdirSync(scenarioArtifactsDir, { recursive: true });
1082
1224
  }
1083
1225
  const captureEngine = new CaptureEngine(scenarioArtifactsDir);
1084
1226
  const consoleTracker = new ConsoleTracker();
1085
1227
  const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
1086
- let currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1087
- let desktopDriver = null;
1228
+ const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1088
1229
  let previewDriver = null;
1089
1230
  let processManager = null;
1231
+ let customDriverStop = null;
1232
+ const pluginManager = new PluginManager();
1233
+ const pluginSpecs = [...options.plugins || [], ...scenario.plugins || []];
1234
+ if (pluginSpecs.length > 0) {
1235
+ await pluginManager.loadAll(pluginSpecs);
1236
+ }
1237
+ const hookContext = {
1238
+ scenario,
1239
+ targetMode,
1240
+ artifactsDir: scenarioArtifactsDir,
1241
+ cliOptions: options,
1242
+ state: /* @__PURE__ */ new Map()
1243
+ };
1090
1244
  try {
1091
1245
  let page;
1092
1246
  let context;
@@ -1103,32 +1257,31 @@ async function runVisualScenario(options) {
1103
1257
  const waitTarget = options.url || "http://localhost:5173";
1104
1258
  await processManager.waitForUrl(waitTarget);
1105
1259
  }
1260
+ await pluginManager.runSetup(hookContext);
1106
1261
  if (typeof scenario.setup === "function") {
1107
1262
  console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
1108
1263
  await scenario.setup();
1109
1264
  }
1110
- if (targetMode === "desktop") {
1111
- desktopDriver = new DesktopDriver({
1112
- port: options.port || 9222,
1113
- autoLaunch: options.autoLaunchDesktop ?? true,
1114
- executablePath: options.executablePath,
1115
- args: options.desktopArgs,
1116
- env: options.desktopEnv
1117
- });
1118
- const res = await desktopDriver.start(currentViewport);
1119
- page = res.page;
1120
- context = res.context;
1265
+ const customSession = await pluginManager.launchSession(
1266
+ { currentViewport, headed: options.headed },
1267
+ hookContext
1268
+ );
1269
+ if (customSession) {
1270
+ page = customSession.page;
1271
+ context = customSession.context;
1272
+ if (customSession.stop) {
1273
+ customDriverStop = customSession.stop;
1274
+ }
1275
+ } else if (targetMode === "desktop") {
1276
+ throw new Error(
1277
+ `[AgentLens] Target mode is 'desktop', but no desktop plugin is registered. Please add --plugin=desktop-webview2 to your command or configuration.`
1278
+ );
1121
1279
  } else {
1122
1280
  previewDriver = new PreviewDriver({
1123
1281
  wwwrootDir: options.wwwrootDir,
1124
1282
  url: options.url,
1125
1283
  headed: options.headed
1126
1284
  });
1127
- const mergedMocks = [...options.globalMocks || [], ...scenario.mockIpc || []];
1128
- if (mergedMocks.length > 0) {
1129
- console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
1130
- previewDriver.mockRegistry.setBatch(mergedMocks);
1131
- }
1132
1285
  if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1133
1286
  console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
1134
1287
  await previewDriver.setupRouteMocks(scenario.mockRoutes);
@@ -1140,194 +1293,27 @@ async function runVisualScenario(options) {
1140
1293
  await previewDriver.setupRouteMocks(scenario.mockRoutes);
1141
1294
  }
1142
1295
  }
1296
+ await pluginManager.runOnContextCreated(context, hookContext);
1297
+ await pluginManager.runOnPageCreated(page, context, hookContext);
1143
1298
  consoleTracker.attach(page);
1144
1299
  console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
1145
1300
  `);
1146
- const doNavigate = async (route) => {
1147
- if (route.startsWith("http://") || route.startsWith("https://")) {
1148
- await page.goto(route, { waitUntil: "domcontentloaded" });
1149
- await page.waitForTimeout(300);
1150
- return;
1151
- }
1152
- if (route.startsWith("#")) {
1153
- await page.evaluate((r) => {
1154
- window.location.hash = r;
1155
- }, route);
1156
- await page.waitForTimeout(300);
1157
- return;
1158
- }
1159
- if (previewDriver?.baseUrl && options.url) {
1160
- try {
1161
- const fullUrl = new URL(route, previewDriver.baseUrl).toString();
1162
- await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
1163
- await page.waitForTimeout(300);
1164
- return;
1165
- } catch {
1166
- }
1167
- }
1168
- const routePath = route.startsWith("/") ? route : `/${route}`;
1169
- await page.evaluate((r) => {
1170
- if (window.location.hash !== void 0) {
1171
- window.location.hash = r;
1172
- }
1173
- }, routePath);
1174
- await page.waitForTimeout(300);
1175
- };
1301
+ const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
1176
1302
  if (scenario.route) {
1177
- console.log(`[Runner] Navigating to route: ${scenario.route}`);
1303
+ console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
1178
1304
  await doNavigate(scenario.route);
1179
1305
  }
1180
- const ctx = {
1306
+ const { ctx } = buildTestContext({
1181
1307
  page,
1182
1308
  context,
1183
1309
  targetMode,
1184
- currentViewport,
1185
- capture: async (name, opts) => {
1186
- console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
1187
- return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
1188
- },
1189
- captureBurst: async (name, opts) => {
1190
- console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
1191
- return await captureEngine.takeBurst(page, name, currentViewport, opts);
1192
- },
1193
- navigate: async (route) => {
1194
- console.log(`\u{1F9ED} [Navigate] ${route}`);
1195
- await doNavigate(route);
1196
- },
1197
- // ─── Viewport ───
1198
- resize: async (width, height) => {
1199
- console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1200
- currentViewport = { width, height };
1201
- ctx.currentViewport = currentViewport;
1202
- await page.setViewportSize(currentViewport);
1203
- await page.waitForTimeout(200);
1204
- },
1205
- setPreset: async (preset) => {
1206
- console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1207
- await ctx.resize(preset.width, preset.height);
1208
- },
1209
- resizeToFit: async (selector, padding = 0) => {
1210
- let boundingBox;
1211
- if (selector) {
1212
- const el = await page.$(selector);
1213
- if (el) {
1214
- boundingBox = await el.boundingBox();
1215
- }
1216
- } else {
1217
- boundingBox = await page.evaluate(() => {
1218
- return {
1219
- width: document.documentElement.scrollWidth,
1220
- height: document.documentElement.scrollHeight
1221
- };
1222
- });
1223
- }
1224
- if (boundingBox) {
1225
- const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1226
- const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1227
- console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1228
- await ctx.resize(newWidth, newHeight);
1229
- } else {
1230
- console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1231
- }
1232
- },
1233
- wait: async (ms) => {
1234
- await page.waitForTimeout(ms);
1235
- },
1236
- waitForSelector: async (selector, timeoutMs = 5e3) => {
1237
- await page.waitForSelector(selector, { timeout: timeoutMs });
1238
- },
1239
- click: async (selector) => {
1240
- console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1241
- await page.click(selector);
1242
- },
1243
- rightClick: async (selector) => {
1244
- console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1245
- await page.click(selector, { button: "right" });
1246
- },
1247
- type: async (selector, text) => {
1248
- console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1249
- await page.fill(selector, text);
1250
- },
1251
- selectOption: async (selector, value) => {
1252
- console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1253
- await page.selectOption(selector, value);
1254
- },
1255
- hover: async (selector) => {
1256
- console.log(`\u{1F446} [Hover] ${selector}`);
1257
- await page.hover(selector);
1258
- },
1259
- scroll: async (selector, deltaY) => {
1260
- console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1261
- await page.evaluate(({ sel, dY }) => {
1262
- const el = document.querySelector(sel);
1263
- if (el) {
1264
- el.scrollTop += dY;
1265
- } else {
1266
- window.scrollBy(0, dY);
1267
- }
1268
- }, { sel: selector, dY: deltaY });
1269
- await page.waitForTimeout(100);
1270
- },
1271
- log: (msg) => {
1272
- console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1273
- },
1274
- // ─── Mock IPC ───
1275
- setMockIpc: async (action, data, mockOptions) => {
1276
- if (targetMode === "desktop") {
1277
- console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
1278
- return;
1279
- }
1280
- if (!previewDriver) {
1281
- console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
1282
- return;
1283
- }
1284
- console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
1285
- await previewDriver.updateMockIpc(action, data, mockOptions);
1286
- },
1287
- setMockRoute: async (url, body, routeOptions) => {
1288
- if (targetMode === "desktop") {
1289
- console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored in desktop mode`);
1290
- return;
1291
- }
1292
- if (!previewDriver) {
1293
- console.log(`\u26A0\uFE0F [Mock Route] PreviewDriver not available`);
1294
- return;
1295
- }
1296
- console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
1297
- await previewDriver.addRouteMock({
1298
- url,
1299
- body,
1300
- method: routeOptions?.method,
1301
- status: routeOptions?.status,
1302
- delayMs: routeOptions?.delayMs,
1303
- headers: routeOptions?.headers
1304
- });
1305
- },
1306
- getConsoleErrors: () => consoleTracker.getErrors(),
1307
- getConsoleWarnings: () => consoleTracker.getWarnings(),
1308
- hasConsoleErrors: () => consoleTracker.hasErrors,
1309
- // ─── DOM Assertions ───
1310
- readText: async (selector) => {
1311
- const text = await page.textContent(selector);
1312
- return text ? text.trim() : null;
1313
- },
1314
- getPageText: async () => {
1315
- return await page.evaluate(() => document.body.innerText || "");
1316
- },
1317
- isVisible: async (selector) => {
1318
- try {
1319
- const element = await page.$(selector);
1320
- if (!element) return false;
1321
- return await element.isVisible();
1322
- } catch {
1323
- return false;
1324
- }
1325
- },
1326
- getElementCount: async (selector) => {
1327
- const elements = await page.$$(selector);
1328
- return elements.length;
1329
- }
1330
- };
1310
+ initialViewport: currentViewport,
1311
+ captureEngine,
1312
+ consoleTracker,
1313
+ previewDriver,
1314
+ doNavigate
1315
+ });
1316
+ await pluginManager.extendContext(ctx, page, hookContext);
1331
1317
  await scenario.run(ctx);
1332
1318
  const durationMs = Date.now() - startTime;
1333
1319
  const snapshots = captureEngine.getSnapshots();
@@ -1342,38 +1328,19 @@ async function runVisualScenario(options) {
1342
1328
  console.log(`
1343
1329
  \u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
1344
1330
  }
1345
- const reportPath = VisualReporter.generateReport({
1331
+ const reportData = {
1346
1332
  scenario,
1347
1333
  snapshots,
1348
1334
  consoleErrors,
1349
1335
  consoleWarnings,
1350
1336
  outputDir: scenarioArtifactsDir,
1351
1337
  targetMode,
1352
- durationMs
1353
- });
1354
- const syncLatest = (repPath, snaps) => {
1355
- try {
1356
- const latestDir = path6.join(artifactsRoot, "latest");
1357
- if (fs6.existsSync(latestDir)) {
1358
- fs6.rmSync(latestDir, { recursive: true, force: true });
1359
- }
1360
- fs6.mkdirSync(latestDir, { recursive: true });
1361
- fs6.copyFileSync(repPath, path6.join(latestDir, "report.md"));
1362
- const manifestSrc = path6.join(scenarioArtifactsDir, "manifest.json");
1363
- if (fs6.existsSync(manifestSrc)) {
1364
- fs6.copyFileSync(manifestSrc, path6.join(latestDir, "manifest.json"));
1365
- }
1366
- for (const snap of snaps) {
1367
- if (snap.filePath && fs6.existsSync(snap.filePath)) {
1368
- fs6.copyFileSync(snap.filePath, path6.join(latestDir, snap.fileName));
1369
- }
1370
- }
1371
- return path6.join(latestDir, "report.md");
1372
- } catch {
1373
- return void 0;
1374
- }
1338
+ durationMs,
1339
+ customSections: []
1375
1340
  };
1376
- const latestReport = syncLatest(reportPath, snapshots);
1341
+ await pluginManager.runOnAfterRun(reportData, hookContext);
1342
+ const reportPath = VisualReporter.generateReport(reportData);
1343
+ const latestReport = syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1377
1344
  console.log(`
1378
1345
  \u2705 Visual Test Completed Successfully!`);
1379
1346
  console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
@@ -1400,7 +1367,7 @@ async function runVisualScenario(options) {
1400
1367
  const snapshots = captureEngine.getSnapshots();
1401
1368
  const consoleErrors = consoleTracker.getErrors();
1402
1369
  const consoleWarnings = consoleTracker.getWarnings();
1403
- const reportPath = VisualReporter.generateReport({
1370
+ const reportData = {
1404
1371
  scenario,
1405
1372
  snapshots,
1406
1373
  consoleErrors,
@@ -1408,16 +1375,9 @@ async function runVisualScenario(options) {
1408
1375
  outputDir: scenarioArtifactsDir,
1409
1376
  targetMode,
1410
1377
  durationMs
1411
- });
1412
- try {
1413
- const latestDir = path6.join(artifactsRoot, "latest");
1414
- if (fs6.existsSync(latestDir)) {
1415
- fs6.rmSync(latestDir, { recursive: true, force: true });
1416
- }
1417
- fs6.mkdirSync(latestDir, { recursive: true });
1418
- fs6.copyFileSync(reportPath, path6.join(latestDir, "report.md"));
1419
- } catch {
1420
- }
1378
+ };
1379
+ const reportPath = VisualReporter.generateReport(reportData);
1380
+ syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1421
1381
  return {
1422
1382
  scenarioId: scenario.id,
1423
1383
  targetMode,
@@ -1430,6 +1390,7 @@ async function runVisualScenario(options) {
1430
1390
  error: err.message
1431
1391
  };
1432
1392
  } finally {
1393
+ await pluginManager.runTeardown(hookContext);
1433
1394
  if (typeof scenario.teardown === "function") {
1434
1395
  try {
1435
1396
  console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
@@ -1441,10 +1402,10 @@ async function runVisualScenario(options) {
1441
1402
  if (options.cleanPaths && options.cleanPaths.length > 0) {
1442
1403
  for (const cleanTarget of options.cleanPaths) {
1443
1404
  try {
1444
- const resolvedCleanPath = path6.resolve(process.cwd(), cleanTarget);
1445
- if (fs6.existsSync(resolvedCleanPath)) {
1405
+ const resolvedCleanPath = path7.resolve(process.cwd(), cleanTarget);
1406
+ if (fs7.existsSync(resolvedCleanPath)) {
1446
1407
  console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
1447
- fs6.rmSync(resolvedCleanPath, { recursive: true, force: true });
1408
+ fs7.rmSync(resolvedCleanPath, { recursive: true, force: true });
1448
1409
  }
1449
1410
  } catch (cleanErr) {
1450
1411
  console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
@@ -1452,7 +1413,7 @@ async function runVisualScenario(options) {
1452
1413
  }
1453
1414
  }
1454
1415
  if (!options.detach) {
1455
- if (desktopDriver) await desktopDriver.stop();
1416
+ if (customDriverStop) await customDriverStop();
1456
1417
  if (previewDriver) await previewDriver.stop();
1457
1418
  if (processManager) await processManager.stop();
1458
1419
  } else {
@@ -1467,15 +1428,29 @@ var init_runner = __esm({
1467
1428
  init_capture();
1468
1429
  init_reporter();
1469
1430
  init_consoleTracker();
1470
- init_desktopDriver();
1471
1431
  init_previewDriver();
1472
1432
  init_processManager();
1433
+ init_pluginLoader();
1434
+ init_navigation();
1435
+ init_artifactsSync();
1436
+ init_contextBuilder();
1437
+ }
1438
+ });
1439
+
1440
+ // src/features/runner/index.ts
1441
+ var init_runner2 = __esm({
1442
+ "src/features/runner/index.ts"() {
1443
+ "use strict";
1444
+ init_runner();
1445
+ init_navigation();
1446
+ init_artifactsSync();
1447
+ init_contextBuilder();
1473
1448
  }
1474
1449
  });
1475
1450
 
1476
1451
  // src/features/snap/snap.ts
1477
- import path7 from "path";
1478
- import fs7 from "fs";
1452
+ import path8 from "path";
1453
+ import fs8 from "fs";
1479
1454
  function parseViewportPresets(raw) {
1480
1455
  if (!raw || raw.length === 0) {
1481
1456
  return [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
@@ -1522,7 +1497,7 @@ async function runQuickSnap(options) {
1522
1497
  console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
1523
1498
  } else {
1524
1499
  const candidateDir = resolveWwwrootDir();
1525
- if (fs7.existsSync(candidateDir) && fs7.existsSync(path7.join(candidateDir, "index.html"))) {
1500
+ if (fs8.existsSync(candidateDir) && fs8.existsSync(path8.join(candidateDir, "index.html"))) {
1526
1501
  useStaticPreview = true;
1527
1502
  wwwrootDir = candidateDir;
1528
1503
  console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
@@ -1564,7 +1539,7 @@ async function runQuickSnap(options) {
1564
1539
  ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
1565
1540
  await ctx.setPreset(vp);
1566
1541
  await ctx.wait(200);
1567
- await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`);
1542
+ await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
1568
1543
  }
1569
1544
  if (options.selector) {
1570
1545
  ctx.log(`Focusing on selector: "${options.selector}"`);
@@ -1597,7 +1572,8 @@ async function runQuickSnap(options) {
1597
1572
  port: options.port,
1598
1573
  headed: options.headed,
1599
1574
  detach: options.detach,
1600
- artifactsRoot: options.outDir ? path7.resolve(process.cwd(), options.outDir) : void 0
1575
+ artifactsRoot: options.outDir ? path8.resolve(process.cwd(), options.outDir) : void 0,
1576
+ plugins: options.plugins
1601
1577
  });
1602
1578
  console.log(`
1603
1579
  ========================================`);
@@ -1631,109 +1607,118 @@ var init_snap = __esm({
1631
1607
  }
1632
1608
  });
1633
1609
 
1634
- // src/app/cli.ts
1635
- import path8 from "path";
1636
- import fs8 from "fs";
1637
- import { execSync } from "child_process";
1638
- import { createJiti } from "jiti";
1639
- var require_cli = __commonJS({
1640
- "src/app/cli.ts"() {
1641
- init_runner();
1642
- init_config();
1643
- init_snap();
1644
- var fileConfig = loadConfig();
1645
- var args = process.argv.slice(2);
1646
- if (args[0] === "init") {
1647
- initScenarioTemplate();
1648
- process.exit(0);
1649
- }
1650
- var isSnapCommand = args[0] === "snap";
1651
- var effectiveArgs = isSnapCommand ? args.slice(1) : args;
1652
- var options = {
1653
- mode: fileConfig.mode || "preview",
1654
- port: fileConfig.port || 9222,
1655
- headed: fileConfig.headed ?? false,
1656
- detach: fileConfig.detach ?? false,
1657
- build: fileConfig.buildCommand || false,
1658
- start: fileConfig.startCommand,
1659
- startCwd: fileConfig.startCwd,
1660
- cleanArtifacts: fileConfig.cleanArtifacts ?? false,
1661
- url: fileConfig.url,
1662
- exe: fileConfig.executablePath,
1663
- clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
1664
- dir: fileConfig.scenarios,
1665
- wwwroot: fileConfig.wwwroot,
1666
- outDir: fileConfig.outDir
1667
- };
1668
- for (const arg of effectiveArgs) {
1669
- if (arg === "--help" || arg === "-h") {
1670
- options.help = true;
1671
- } else if (arg.startsWith("--scenario=")) {
1672
- options.scenario = arg.split("=")[1];
1673
- } else if (arg === "--all") {
1674
- options.all = true;
1675
- } else if (arg.startsWith("--mode=")) {
1676
- const mode = arg.split("=")[1].toLowerCase();
1677
- if (mode === "desktop" || mode === "preview") {
1678
- options.mode = mode;
1679
- }
1680
- } else if (arg.startsWith("--port=")) {
1681
- options.port = parseInt(arg.split("=")[1], 10) || 9222;
1682
- } else if (arg === "--headed") {
1683
- options.headed = true;
1684
- } else if (arg === "--detach") {
1685
- options.detach = true;
1686
- } else if (arg === "--build") {
1687
- options.build = true;
1688
- } else if (arg.startsWith("--build=")) {
1689
- options.build = arg.slice("--build=".length);
1690
- } else if (arg.startsWith("--start=")) {
1691
- options.start = arg.slice("--start=".length);
1692
- } else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
1693
- options.startCwd = arg.split("=")[1];
1694
- } else if (arg === "--clean-artifacts") {
1695
- options.cleanArtifacts = true;
1696
- } else if (arg.startsWith("--url=")) {
1697
- options.url = arg.split("=")[1];
1698
- } else if (arg.startsWith("--selector=")) {
1699
- options.selector = arg.split("=")[1];
1700
- } else if (arg.startsWith("--viewports=")) {
1701
- const raw = arg.slice("--viewports=".length);
1702
- options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
1703
- } else if (arg.startsWith("--wait=")) {
1704
- options.waitMs = parseInt(arg.slice("--wait=".length), 10);
1705
- } else if (arg.startsWith("--name=")) {
1706
- options.name = arg.slice("--name=".length);
1707
- } else if (arg.startsWith("--exe=")) {
1708
- options.exe = arg.slice("--exe=".length);
1709
- } else if (arg.startsWith("--executable=")) {
1710
- options.exe = arg.slice("--executable=".length);
1711
- } else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
1712
- const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
1713
- const rawPaths = arg.slice(prefix.length);
1714
- options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
1715
- } else if (arg.startsWith("--dir=")) {
1716
- options.dir = arg.split("=")[1];
1717
- } else if (arg.startsWith("--wwwroot=")) {
1718
- options.wwwroot = arg.split("=")[1];
1719
- } else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
1720
- options.outDir = arg.split("=")[1];
1721
- }
1722
- }
1723
- if (options.start && !options.startCwd) {
1724
- options.startCwd = detectStartCwd(options.startCwd);
1610
+ // src/app/lib/argsParser.ts
1611
+ function parseCliArgs(argv, fileConfig) {
1612
+ const isInitCommand = argv[0] === "init";
1613
+ const isSnapCommand = argv[0] === "snap";
1614
+ const effectiveArgs = isSnapCommand ? argv.slice(1) : argv;
1615
+ const options = {
1616
+ mode: fileConfig.mode || "preview",
1617
+ port: fileConfig.port || 9222,
1618
+ headed: fileConfig.headed ?? false,
1619
+ detach: fileConfig.detach ?? false,
1620
+ build: fileConfig.buildCommand || false,
1621
+ start: fileConfig.startCommand,
1622
+ startCwd: fileConfig.startCwd,
1623
+ cleanArtifacts: fileConfig.cleanArtifacts ?? false,
1624
+ url: fileConfig.url,
1625
+ exe: fileConfig.executablePath,
1626
+ clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
1627
+ dir: fileConfig.scenarios,
1628
+ wwwroot: fileConfig.wwwroot,
1629
+ outDir: fileConfig.outDir,
1630
+ plugins: fileConfig.plugins ? [...fileConfig.plugins] : []
1631
+ };
1632
+ for (const arg of effectiveArgs) {
1633
+ if (arg === "--help" || arg === "-h") {
1634
+ options.help = true;
1635
+ } else if (arg.startsWith("--scenario=")) {
1636
+ options.scenario = arg.split("=")[1];
1637
+ } else if (arg === "--all") {
1638
+ options.all = true;
1639
+ } else if (arg.startsWith("--mode=")) {
1640
+ options.mode = arg.split("=")[1].toLowerCase();
1641
+ } else if (arg.startsWith("--port=")) {
1642
+ options.port = parseInt(arg.split("=")[1], 10) || 9222;
1643
+ } else if (arg === "--headed") {
1644
+ options.headed = true;
1645
+ } else if (arg === "--detach") {
1646
+ options.detach = true;
1647
+ } else if (arg === "--build") {
1648
+ options.build = true;
1649
+ } else if (arg.startsWith("--build=")) {
1650
+ options.build = arg.slice("--build=".length);
1651
+ } else if (arg.startsWith("--start=")) {
1652
+ options.start = arg.slice("--start=".length);
1653
+ } else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
1654
+ options.startCwd = arg.split("=")[1];
1655
+ } else if (arg === "--clean-artifacts") {
1656
+ options.cleanArtifacts = true;
1657
+ } else if (arg.startsWith("--url=")) {
1658
+ options.url = arg.split("=")[1];
1659
+ } else if (arg.startsWith("--selector=")) {
1660
+ options.selector = arg.split("=")[1];
1661
+ } else if (arg.startsWith("--viewports=")) {
1662
+ const raw = arg.slice("--viewports=".length);
1663
+ options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
1664
+ } else if (arg.startsWith("--wait=")) {
1665
+ options.waitMs = parseInt(arg.slice("--wait=".length), 10);
1666
+ } else if (arg.startsWith("--name=")) {
1667
+ options.name = arg.slice("--name=".length);
1668
+ } else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
1669
+ const prefix = arg.startsWith("--exe=") ? "--exe=" : "--executable=";
1670
+ options.exe = arg.slice(prefix.length);
1671
+ } else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
1672
+ const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
1673
+ const rawPaths = arg.slice(prefix.length);
1674
+ options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
1675
+ } else if (arg.startsWith("--dir=")) {
1676
+ options.dir = arg.split("=")[1];
1677
+ } else if (arg.startsWith("--wwwroot=")) {
1678
+ options.wwwroot = arg.split("=")[1];
1679
+ } else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
1680
+ options.outDir = arg.split("=")[1];
1681
+ } else if (arg.startsWith("--plugin=") || arg.startsWith("--plugins=")) {
1682
+ const raw = arg.split("=")[1] || "";
1683
+ const items = raw.split(",").map((p) => p.trim()).filter(Boolean);
1684
+ options.plugins = [...options.plugins || [], ...items];
1685
+ } else if (arg === "--full" || arg === "--full-page") {
1686
+ options.fullPage = true;
1725
1687
  }
1726
- function printHelp() {
1727
- console.log(`
1688
+ }
1689
+ if (options.start && !options.startCwd) {
1690
+ options.startCwd = detectStartCwd(options.startCwd);
1691
+ }
1692
+ if ((options.mode === "desktop" || options.exe) && !options.plugins?.includes("desktop-webview2")) {
1693
+ options.plugins = ["desktop-webview2", ...options.plugins || []];
1694
+ }
1695
+ return {
1696
+ isSnapCommand,
1697
+ isInitCommand,
1698
+ options
1699
+ };
1700
+ }
1701
+ var init_argsParser = __esm({
1702
+ "src/app/lib/argsParser.ts"() {
1703
+ "use strict";
1704
+ init_config();
1705
+ }
1706
+ });
1707
+
1708
+ // src/app/lib/help.ts
1709
+ function printHelp() {
1710
+ console.log(`
1728
1711
  \u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
1729
1712
 
1730
1713
  Usage:
1731
1714
  npx agent-lens snap [options] Instant one-shot visual & console check (no test files needed)
1715
+ npx agent-lens live [action] Interactive live session (start, click, type, snap, stop)
1732
1716
  npx agent-lens [options] Run scripted scenario tests from scenarios/
1733
1717
  npx agent-lens init Generate starter scenario template & mocks
1734
1718
 
1735
1719
  Commands:
1736
1720
  snap Take immediate multi-viewport screenshots of a URL & check console errors
1721
+ live Interactive control: click coordinates/selectors, type text, snap --full
1737
1722
  init Generate starter template in scenarios/template.scenario.ts and mocks.ts
1738
1723
 
1739
1724
  Options:
@@ -1741,6 +1726,7 @@ Options:
1741
1726
  --start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
1742
1727
  --start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
1743
1728
  --clean-artifacts Purge previous test artifacts to prevent folder bloat
1729
+ --full Capture full scrollable page instead of only the viewport
1744
1730
  --selector=<css> Target a specific element to focus on / resize-to-fit
1745
1731
  --viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
1746
1732
  --wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
@@ -1752,6 +1738,7 @@ Options:
1752
1738
  --port=<port> CDP remote debugging port [default: 9222]
1753
1739
  --build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
1754
1740
  --clean=<paths> Comma-separated paths to safely delete upon test exit (e.g. --clean="./temp,./cache")
1741
+ --plugin=<names> Comma-separated plugins (e.g. --plugin=desktop-webview2,mock-ipc)
1755
1742
  --headed Show Chromium browser window
1756
1743
  --detach Keep browser/app open after finishing
1757
1744
  --dir=<path> Custom scenarios directory [default: scenarios]
@@ -1759,15 +1746,24 @@ Options:
1759
1746
  --folder=<path> Directory to save visual artifacts/reports (also --outDir)
1760
1747
  --help, -h Show this help message
1761
1748
  `);
1762
- }
1763
- function initScenarioTemplate() {
1764
- const targetDir = path8.resolve(process.cwd(), "scenarios");
1765
- if (!fs8.existsSync(targetDir)) {
1766
- fs8.mkdirSync(targetDir, { recursive: true });
1767
- }
1768
- const templatePath = path8.join(targetDir, "template.scenario.ts");
1769
- if (!fs8.existsSync(templatePath)) {
1770
- const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
1749
+ }
1750
+ var init_help = __esm({
1751
+ "src/app/lib/help.ts"() {
1752
+ "use strict";
1753
+ }
1754
+ });
1755
+
1756
+ // src/app/lib/templateInit.ts
1757
+ import path9 from "path";
1758
+ import fs9 from "fs";
1759
+ function initScenarioTemplate() {
1760
+ const targetDir = path9.resolve(process.cwd(), "scenarios");
1761
+ if (!fs9.existsSync(targetDir)) {
1762
+ fs9.mkdirSync(targetDir, { recursive: true });
1763
+ }
1764
+ const templatePath = path9.join(targetDir, "template.scenario.ts");
1765
+ if (!fs9.existsSync(templatePath)) {
1766
+ const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
1771
1767
 
1772
1768
  export default defineVisualTest({
1773
1769
  id: 'template-check',
@@ -1797,14 +1793,14 @@ export default defineVisualTest({
1797
1793
  }
1798
1794
  });
1799
1795
  `;
1800
- fs8.writeFileSync(templatePath, templateContent, "utf8");
1801
- console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
1802
- } else {
1803
- console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
1804
- }
1805
- const mocksPath = path8.join(targetDir, "mocks.ts");
1806
- if (!fs8.existsSync(mocksPath)) {
1807
- const mocksContent = `/**
1796
+ fs9.writeFileSync(templatePath, templateContent, "utf8");
1797
+ console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
1798
+ } else {
1799
+ console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
1800
+ }
1801
+ const mocksPath = path9.join(targetDir, "mocks.ts");
1802
+ if (!fs9.existsSync(mocksPath)) {
1803
+ const mocksContent = `/**
1808
1804
  * Global IPC & API Mocks
1809
1805
  *
1810
1806
  * Export an array of base mocks to satisfy root application state on boot.
@@ -1814,160 +1810,408 @@ export default [
1814
1810
  { action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
1815
1811
  ];
1816
1812
  `;
1817
- fs8.writeFileSync(mocksPath, mocksContent, "utf8");
1818
- console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
1813
+ fs9.writeFileSync(mocksPath, mocksContent, "utf8");
1814
+ console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
1815
+ }
1816
+ console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
1817
+ }
1818
+ var init_templateInit = __esm({
1819
+ "src/app/lib/templateInit.ts"() {
1820
+ "use strict";
1821
+ }
1822
+ });
1823
+
1824
+ // src/app/lib/scenarioFinder.ts
1825
+ import path10 from "path";
1826
+ import fs10 from "fs";
1827
+ async function findScenarios(dir, specificName) {
1828
+ const results = [];
1829
+ if (!fs10.existsSync(dir)) return results;
1830
+ const items = fs10.readdirSync(dir, { withFileTypes: true });
1831
+ for (const item of items) {
1832
+ const fullPath = path10.join(dir, item.name);
1833
+ if (item.isDirectory()) {
1834
+ results.push(...await findScenarios(fullPath, specificName));
1835
+ } else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
1836
+ if (!specificName || item.name.startsWith(specificName)) {
1837
+ results.push(fullPath);
1819
1838
  }
1820
- console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
1821
1839
  }
1822
- if (options.help) {
1823
- printHelp();
1824
- process.exit(0);
1840
+ }
1841
+ return results;
1842
+ }
1843
+ function resolveScenariosDirectory(customDir) {
1844
+ if (customDir) {
1845
+ return path10.resolve(process.cwd(), customDir);
1846
+ }
1847
+ const candidates = [
1848
+ "scenarios",
1849
+ "tests/visual",
1850
+ "tests/scenarios",
1851
+ "test/scenarios",
1852
+ "src/scenarios"
1853
+ ];
1854
+ for (const c of candidates) {
1855
+ const p = path10.resolve(process.cwd(), c);
1856
+ if (fs10.existsSync(p)) {
1857
+ return p;
1825
1858
  }
1826
- async function findScenarios(dir, specificName) {
1827
- const results = [];
1828
- if (!fs8.existsSync(dir)) return results;
1829
- const items = fs8.readdirSync(dir, { withFileTypes: true });
1830
- for (const item of items) {
1831
- const fullPath = path8.join(dir, item.name);
1832
- if (item.isDirectory()) {
1833
- results.push(...await findScenarios(fullPath, specificName));
1834
- } else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
1835
- if (!specificName || item.name.startsWith(specificName)) {
1836
- results.push(fullPath);
1837
- }
1838
- }
1839
- }
1840
- return results;
1859
+ }
1860
+ return path10.resolve(process.cwd(), "scenarios");
1861
+ }
1862
+ var init_scenarioFinder = __esm({
1863
+ "src/app/lib/scenarioFinder.ts"() {
1864
+ "use strict";
1865
+ }
1866
+ });
1867
+
1868
+ // src/plugins/live-controller/session.ts
1869
+ import fs11 from "fs";
1870
+ import path11 from "path";
1871
+ import treeKill2 from "tree-kill";
1872
+ function getLiveArtifactsDir() {
1873
+ if (!fs11.existsSync(LIVE_ARTIFACTS_DIR)) {
1874
+ fs11.mkdirSync(LIVE_ARTIFACTS_DIR, { recursive: true });
1875
+ }
1876
+ return LIVE_ARTIFACTS_DIR;
1877
+ }
1878
+ function readLiveSession() {
1879
+ if (!fs11.existsSync(SESSION_FILE)) return null;
1880
+ try {
1881
+ return JSON.parse(fs11.readFileSync(SESSION_FILE, "utf-8"));
1882
+ } catch {
1883
+ return null;
1884
+ }
1885
+ }
1886
+ function saveLiveSession(info) {
1887
+ if (!fs11.existsSync(SESSION_DIR)) {
1888
+ fs11.mkdirSync(SESSION_DIR, { recursive: true });
1889
+ }
1890
+ fs11.writeFileSync(SESSION_FILE, JSON.stringify(info, null, 2), "utf-8");
1891
+ }
1892
+ function clearLiveSession() {
1893
+ if (fs11.existsSync(SESSION_FILE)) {
1894
+ try {
1895
+ fs11.unlinkSync(SESSION_FILE);
1896
+ } catch {
1841
1897
  }
1842
- function resolveScenariosDirectory(customDir) {
1843
- if (customDir) {
1844
- return path8.resolve(process.cwd(), customDir);
1845
- }
1846
- const candidates = [
1847
- "scenarios",
1848
- "tests/visual",
1849
- "tests/scenarios",
1850
- "test/scenarios",
1851
- "src/scenarios"
1852
- ];
1853
- for (const c of candidates) {
1854
- const p = path8.resolve(process.cwd(), c);
1855
- if (fs8.existsSync(p)) {
1856
- return p;
1857
- }
1858
- }
1859
- return path8.resolve(process.cwd(), "scenarios");
1898
+ }
1899
+ }
1900
+ async function connectToLiveSession() {
1901
+ const session = readLiveSession();
1902
+ if (!session) {
1903
+ throw new Error(
1904
+ `No active live session found. Start one with: npx agent-lens live start --url=http://localhost:5173`
1905
+ );
1906
+ }
1907
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${session.port}`);
1908
+ const contexts = browser.contexts();
1909
+ const context = contexts[0] || await browser.newContext();
1910
+ const pages = context.pages();
1911
+ const page = pages[0] || await context.waitForEvent("page", { timeout: 5e3 });
1912
+ return { browser, context, page };
1913
+ }
1914
+ async function stopLiveSession() {
1915
+ const session = readLiveSession();
1916
+ if (!session) {
1917
+ console.log(`\u2139\uFE0F [Live] No active session to stop.`);
1918
+ return false;
1919
+ }
1920
+ try {
1921
+ const { browser } = await connectToLiveSession();
1922
+ await browser.close().catch(() => {
1923
+ });
1924
+ } catch {
1925
+ }
1926
+ if (session.pid) {
1927
+ await new Promise((resolve) => {
1928
+ treeKill2(session.pid, "SIGTERM", () => resolve());
1929
+ });
1930
+ }
1931
+ clearLiveSession();
1932
+ console.log(`\u{1F6D1} [Live] Session stopped and cleaned up.`);
1933
+ return true;
1934
+ }
1935
+ var SESSION_DIR, SESSION_FILE, LIVE_ARTIFACTS_DIR;
1936
+ var init_session = __esm({
1937
+ "src/plugins/live-controller/session.ts"() {
1938
+ "use strict";
1939
+ init_playwrightLoader();
1940
+ SESSION_DIR = path11.resolve(process.cwd(), ".agent-lens");
1941
+ SESSION_FILE = path11.join(SESSION_DIR, "live-session.json");
1942
+ LIVE_ARTIFACTS_DIR = path11.resolve(process.cwd(), "artifacts", "live");
1943
+ }
1944
+ });
1945
+
1946
+ // src/plugins/live-controller/actions.ts
1947
+ import path12 from "path";
1948
+ async function clickCoords(page, x, y, options) {
1949
+ console.log(`\u{1F5B1}\uFE0F [LiveController] Clicking coordinates: (${x}, ${y})`);
1950
+ await page.mouse.click(x, y, options);
1951
+ await page.waitForTimeout(200);
1952
+ }
1953
+ async function snapLive(page, options) {
1954
+ const artifactsDir = getLiveArtifactsDir();
1955
+ const fileName = options?.name ? `${options.name}.png` : "current.png";
1956
+ const filePath = path12.join(artifactsDir, fileName);
1957
+ if (options?.selector) {
1958
+ const el = await page.waitForSelector(options.selector, { timeout: 5e3 });
1959
+ await el.screenshot({ path: filePath });
1960
+ } else {
1961
+ await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
1962
+ }
1963
+ console.log(`\u{1F4F8} [LiveController] Snapshot saved: ${filePath}${options?.fullPage ? " (Full Page)" : ""}`);
1964
+ return filePath;
1965
+ }
1966
+ async function handleLiveCli(argv) {
1967
+ const action = argv[0]?.toLowerCase();
1968
+ switch (action) {
1969
+ case "start": {
1970
+ const urlArg = argv.find((a) => a.startsWith("--url="))?.split("=")[1] || "http://localhost:5173";
1971
+ const portArg = parseInt(argv.find((a) => a.startsWith("--port="))?.split("=")[1] || "9223", 10);
1972
+ const isHeaded = argv.includes("--headed");
1973
+ console.log(`\u{1F680} [Live] Starting background browser for ${urlArg} on CDP port ${portArg}...`);
1974
+ const browser = await chromium.launch({
1975
+ headless: !isHeaded,
1976
+ args: [`--remote-debugging-port=${portArg}`, "--no-sandbox"]
1977
+ });
1978
+ const context = await browser.newContext({ viewport: { width: 1200, height: 800 } });
1979
+ const page = await context.newPage();
1980
+ await page.goto(urlArg, { waitUntil: "domcontentloaded" });
1981
+ await page.waitForTimeout(500);
1982
+ saveLiveSession({
1983
+ port: portArg,
1984
+ url: urlArg,
1985
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1986
+ });
1987
+ await snapLive(page, { name: "current" });
1988
+ console.log(`\u2705 [Live] Session active! You can now send live commands:
1989
+ `);
1990
+ console.log(` npx agent-lens live click 450 120`);
1991
+ console.log(` npx agent-lens live type "input" "hello"`);
1992
+ console.log(` npx agent-lens live snap --full`);
1993
+ console.log(` npx agent-lens live stop
1994
+ `);
1995
+ return true;
1860
1996
  }
1861
- async function main() {
1862
- if (isSnapCommand || options.url && !options.scenario && !options.all) {
1863
- const snapSuccess = await runQuickSnap({
1864
- url: options.url,
1865
- start: options.start,
1866
- startCwd: options.startCwd,
1867
- selector: options.selector,
1868
- viewports: options.viewports,
1869
- waitMs: options.waitMs,
1870
- name: options.name,
1871
- clean: options.clean,
1872
- cleanArtifacts: options.cleanArtifacts,
1873
- headed: options.headed,
1874
- detach: options.detach,
1875
- outDir: options.outDir,
1876
- mode: options.mode,
1877
- exe: options.exe,
1878
- port: options.port
1879
- });
1880
- process.exit(snapSuccess ? 0 : 1);
1997
+ case "click": {
1998
+ const { page, browser } = await connectToLiveSession();
1999
+ const firstArg = argv[1];
2000
+ const secondArg = argv[2];
2001
+ const x = parseInt(firstArg, 10);
2002
+ const y = parseInt(secondArg, 10);
2003
+ if (!isNaN(x) && !isNaN(y)) {
2004
+ await clickCoords(page, x, y);
2005
+ } else if (firstArg) {
2006
+ console.log(`\u{1F5B1}\uFE0F [Live] Clicking selector: "${firstArg}"`);
2007
+ await page.click(firstArg);
2008
+ } else {
2009
+ console.error(`\u274C Usage: npx agent-lens live click <x> <y> OR npx agent-lens live click <selector>`);
2010
+ await browser.close();
2011
+ return false;
1881
2012
  }
1882
- if (!options.scenario && !options.all) {
1883
- console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
1884
- printHelp();
1885
- process.exit(1);
2013
+ await snapLive(page, { name: "current" });
2014
+ return true;
2015
+ }
2016
+ case "type": {
2017
+ const { page, browser } = await connectToLiveSession();
2018
+ const selector = argv[1];
2019
+ const text = argv[2];
2020
+ if (!selector || text === void 0) {
2021
+ console.error(`\u274C Usage: npx agent-lens live type <selector> <text>`);
2022
+ await browser.close();
2023
+ return false;
1886
2024
  }
1887
- const scenariosDir = resolveScenariosDirectory(options.dir);
1888
- const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
1889
- if (scenarioPaths.length === 0) {
1890
- console.error(`\u274C No scenarios found in ${scenariosDir}`);
1891
- console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
2025
+ console.log(`\u2328\uFE0F [Live] Typing into "${selector}": "${text}"`);
2026
+ await page.fill(selector, text);
2027
+ await snapLive(page, { name: "current" });
2028
+ return true;
2029
+ }
2030
+ case "snap": {
2031
+ const { page } = await connectToLiveSession();
2032
+ const isFull = argv.includes("--full");
2033
+ const nameArg = argv.find((a) => !a.startsWith("--") && a !== "snap");
2034
+ await snapLive(page, { name: nameArg || "current", fullPage: isFull });
2035
+ return true;
2036
+ }
2037
+ case "stop": {
2038
+ return await stopLiveSession();
2039
+ }
2040
+ default: {
2041
+ console.log(`
2042
+ \u2139\uFE0F AgentLens Live Controller:
2043
+ npx agent-lens live start --url=<url> Start background live session
2044
+ npx agent-lens live click <x> <y> Click at pixel coordinates
2045
+ npx agent-lens live click <selector> Click CSS selector
2046
+ npx agent-lens live type <sel> <text> Fill input field
2047
+ npx agent-lens live snap [name] [--full] Capture current or full-page screenshot
2048
+ npx agent-lens live stop Close live browser and finish
2049
+ `);
2050
+ return true;
2051
+ }
2052
+ }
2053
+ }
2054
+ var init_actions = __esm({
2055
+ "src/plugins/live-controller/actions.ts"() {
2056
+ "use strict";
2057
+ init_session();
2058
+ init_playwrightLoader();
2059
+ }
2060
+ });
2061
+
2062
+ // src/app/cli.ts
2063
+ import path13 from "path";
2064
+ import fs12 from "fs";
2065
+ import { execSync } from "child_process";
2066
+ import { createJiti as createJiti2 } from "jiti";
2067
+ var require_cli = __commonJS({
2068
+ "src/app/cli.ts"() {
2069
+ init_config();
2070
+ init_runner2();
2071
+ init_snap();
2072
+ init_argsParser();
2073
+ init_help();
2074
+ init_templateInit();
2075
+ init_scenarioFinder();
2076
+ init_actions();
2077
+ var rawArgs = process.argv.slice(2);
2078
+ if (rawArgs[0] === "live") {
2079
+ handleLiveCli(rawArgs.slice(1)).then((success) => process.exit(success ? 0 : 1)).catch((err) => {
2080
+ console.error("\u274C Live command failed:", err instanceof Error ? err.message : err);
1892
2081
  process.exit(1);
2082
+ });
2083
+ } else {
2084
+ const fileConfig = loadConfig();
2085
+ const { isSnapCommand, isInitCommand, options } = parseCliArgs(rawArgs, fileConfig);
2086
+ if (isInitCommand) {
2087
+ initScenarioTemplate();
2088
+ process.exit(0);
1893
2089
  }
1894
- const shouldBuild = options.build || fileConfig.autoBuild;
1895
- if (shouldBuild) {
1896
- const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
1897
- console.log(`
1898
- \u{1F528} [Build] Running build process: "${buildCmd}"...`);
1899
- try {
1900
- execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
1901
- } catch (e) {
1902
- console.error(`\u274C Build failed: ${buildCmd}`);
1903
- process.exit(1);
1904
- }
1905
- } else if (options.mode === "preview" && !options.url && !options.start) {
1906
- console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
1907
- }
1908
- console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
1909
- const jiti = createJiti(process.cwd());
1910
- let globalMocks = [];
1911
- const mockCandidates = [
1912
- path8.join(scenariosDir, "mocks.ts"),
1913
- path8.join(scenariosDir, "mocks.js"),
1914
- path8.join(process.cwd(), "mocks.ts"),
1915
- path8.join(process.cwd(), "mocks.js")
1916
- ];
1917
- for (const mocksPath of mockCandidates) {
1918
- if (fs8.existsSync(mocksPath)) {
1919
- try {
1920
- const m = await jiti.import(mocksPath);
1921
- globalMocks = m.default || m.mocks || [];
1922
- console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${path8.basename(mocksPath)}`);
1923
- break;
1924
- } catch (err) {
1925
- console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
1926
- }
1927
- }
2090
+ if (options.help) {
2091
+ printHelp();
2092
+ process.exit(0);
1928
2093
  }
1929
- let allSuccess = true;
1930
- for (const scenarioPath of scenarioPaths) {
1931
- try {
1932
- const scenarioModule = await jiti.import(scenarioPath);
1933
- const scenario = scenarioModule.default || scenarioModule.scenario;
1934
- if (!scenario || typeof scenario.run !== "function") {
1935
- console.error(`\u26A0\uFE0F Skipped: file ${path8.basename(scenarioPath)} does not export a VisualScenario object by default.`);
1936
- continue;
1937
- }
1938
- const result = await runVisualScenario({
1939
- scenario,
1940
- targetMode: options.mode,
2094
+ async function main() {
2095
+ if (isSnapCommand || options.url && !options.scenario && !options.all) {
2096
+ const snapSuccess = await runQuickSnap({
1941
2097
  url: options.url,
1942
- startCommand: options.start,
2098
+ start: options.start,
1943
2099
  startCwd: options.startCwd,
2100
+ selector: options.selector,
2101
+ viewports: options.viewports,
2102
+ waitMs: options.waitMs,
2103
+ name: options.name,
2104
+ clean: options.clean,
1944
2105
  cleanArtifacts: options.cleanArtifacts,
1945
- port: options.port,
1946
2106
  headed: options.headed,
1947
2107
  detach: options.detach,
1948
- executablePath: options.exe,
1949
- cleanPaths: options.clean,
1950
- desktopEnv: fileConfig.env,
1951
- wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
1952
- artifactsRoot: options.outDir ? path8.resolve(process.cwd(), options.outDir) : void 0,
1953
- globalMocks
2108
+ outDir: options.outDir,
2109
+ mode: options.mode,
2110
+ exe: options.exe,
2111
+ port: options.port,
2112
+ plugins: options.plugins,
2113
+ fullPage: options.fullPage
1954
2114
  });
1955
- if (!result.success) {
2115
+ process.exit(snapSuccess ? 0 : 1);
2116
+ }
2117
+ if (!options.scenario && !options.all) {
2118
+ console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
2119
+ printHelp();
2120
+ process.exit(1);
2121
+ }
2122
+ const scenariosDir = resolveScenariosDirectory(options.dir);
2123
+ const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
2124
+ if (scenarioPaths.length === 0) {
2125
+ console.error(`\u274C No scenarios found in ${scenariosDir}`);
2126
+ console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
2127
+ process.exit(1);
2128
+ }
2129
+ const shouldBuild = options.build || fileConfig.autoBuild;
2130
+ if (shouldBuild) {
2131
+ const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
2132
+ console.log(`
2133
+ \u{1F528} [Build] Running build process: "${buildCmd}"...`);
2134
+ try {
2135
+ execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
2136
+ } catch (e) {
2137
+ console.error(`\u274C Build failed: ${buildCmd}`);
2138
+ process.exit(1);
2139
+ }
2140
+ } else if (options.mode === "preview" && !options.url && !options.start) {
2141
+ console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
2142
+ }
2143
+ console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
2144
+ const jiti = createJiti2(process.cwd());
2145
+ let globalMocks = [];
2146
+ const mockCandidates = [
2147
+ path13.join(scenariosDir, "mocks.ts"),
2148
+ path13.join(scenariosDir, "mocks.js"),
2149
+ path13.join(process.cwd(), "mocks.ts"),
2150
+ path13.join(process.cwd(), "mocks.js")
2151
+ ];
2152
+ for (const mocksPath of mockCandidates) {
2153
+ if (fs12.existsSync(mocksPath)) {
2154
+ try {
2155
+ const m = await jiti.import(mocksPath);
2156
+ globalMocks = m.default || m.mocks || [];
2157
+ console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${path13.basename(mocksPath)}`);
2158
+ break;
2159
+ } catch (err) {
2160
+ console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
2161
+ }
2162
+ }
2163
+ }
2164
+ if (globalMocks.length > 0 && !options.plugins?.includes("mock-ipc")) {
2165
+ options.plugins = ["mock-ipc", ...options.plugins || []];
2166
+ }
2167
+ let allSuccess = true;
2168
+ for (const scenarioPath of scenarioPaths) {
2169
+ try {
2170
+ const scenarioModule = await jiti.import(scenarioPath);
2171
+ const scenario = scenarioModule.default || scenarioModule.scenario;
2172
+ if (!scenario || typeof scenario.run !== "function") {
2173
+ console.error(`\u26A0\uFE0F Skipped: file ${path13.basename(scenarioPath)} does not export a VisualScenario object by default.`);
2174
+ continue;
2175
+ }
2176
+ const scenarioPlugins = [...options.plugins || []];
2177
+ if (scenario.mockIpc && scenario.mockIpc.length > 0 && !scenarioPlugins.includes("mock-ipc")) {
2178
+ scenarioPlugins.push("mock-ipc");
2179
+ }
2180
+ const result = await runVisualScenario({
2181
+ scenario,
2182
+ targetMode: options.mode,
2183
+ url: options.url,
2184
+ startCommand: options.start,
2185
+ startCwd: options.startCwd,
2186
+ cleanArtifacts: options.cleanArtifacts,
2187
+ port: options.port,
2188
+ headed: options.headed,
2189
+ detach: options.detach,
2190
+ executablePath: options.exe,
2191
+ cleanPaths: options.clean,
2192
+ desktopEnv: fileConfig.env,
2193
+ wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
2194
+ artifactsRoot: options.outDir ? path13.resolve(process.cwd(), options.outDir) : void 0,
2195
+ globalMocks,
2196
+ plugins: scenarioPlugins
2197
+ });
2198
+ if (!result.success) {
2199
+ allSuccess = false;
2200
+ }
2201
+ } catch (err) {
2202
+ console.error(`\u274C Failed to load scenario ${path13.basename(scenarioPath)}:`, err);
1956
2203
  allSuccess = false;
1957
2204
  }
1958
- } catch (err) {
1959
- console.error(`\u274C Failed to load scenario ${path8.basename(scenarioPath)}:`, err);
1960
- allSuccess = false;
2205
+ }
2206
+ if (!allSuccess) {
2207
+ process.exit(1);
1961
2208
  }
1962
2209
  }
1963
- if (!allSuccess) {
2210
+ main().catch((err) => {
2211
+ console.error("Fatal error:", err);
1964
2212
  process.exit(1);
1965
- }
2213
+ });
1966
2214
  }
1967
- main().catch((err) => {
1968
- console.error("Fatal error:", err);
1969
- process.exit(1);
1970
- });
1971
2215
  }
1972
2216
  });
1973
2217
  export default require_cli();