@_deep4wee/agent-lens 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +141 -31
  2. package/dist/cli.d.mts +2 -1
  3. package/dist/cli.d.ts +2 -1
  4. package/dist/cli.js +1169 -957
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cli.mjs +1268 -974
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/dsl-BIjVN1M0.d.mts +204 -0
  9. package/dist/dsl-BIjVN1M0.d.ts +204 -0
  10. package/dist/index.d.mts +131 -155
  11. package/dist/index.d.ts +131 -155
  12. package/dist/index.js +1506 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +1479 -3
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/plugins/a11y-tree/index.d.mts +11 -0
  17. package/dist/plugins/a11y-tree/index.d.ts +11 -0
  18. package/dist/plugins/a11y-tree/index.js +190 -0
  19. package/dist/plugins/a11y-tree/index.js.map +1 -0
  20. package/dist/plugins/a11y-tree/index.mjs +155 -0
  21. package/dist/plugins/a11y-tree/index.mjs.map +1 -0
  22. package/dist/plugins/desktop-webview2/index.d.mts +14 -0
  23. package/dist/plugins/desktop-webview2/index.d.ts +14 -0
  24. package/dist/plugins/desktop-webview2/index.js +258 -0
  25. package/dist/plugins/desktop-webview2/index.js.map +1 -0
  26. package/dist/plugins/desktop-webview2/index.mjs +221 -0
  27. package/dist/plugins/desktop-webview2/index.mjs.map +1 -0
  28. package/dist/plugins/live-controller/index.d.mts +35 -0
  29. package/dist/plugins/live-controller/index.d.ts +35 -0
  30. package/dist/plugins/live-controller/index.js +303 -0
  31. package/dist/plugins/live-controller/index.js.map +1 -0
  32. package/dist/plugins/live-controller/index.mjs +261 -0
  33. package/dist/plugins/live-controller/index.mjs.map +1 -0
  34. package/dist/plugins/mock-ipc/index.d.mts +30 -0
  35. package/dist/plugins/mock-ipc/index.d.ts +30 -0
  36. package/dist/plugins/mock-ipc/index.js +210 -0
  37. package/dist/plugins/mock-ipc/index.js.map +1 -0
  38. package/dist/plugins/mock-ipc/index.mjs +181 -0
  39. package/dist/plugins/mock-ipc/index.mjs.map +1 -0
  40. package/dist/plugins/visual-diff/index.d.mts +30 -0
  41. package/dist/plugins/visual-diff/index.d.ts +30 -0
  42. package/dist/plugins/visual-diff/index.js +163 -0
  43. package/dist/plugins/visual-diff/index.js.map +1 -0
  44. package/dist/plugins/visual-diff/index.mjs +127 -0
  45. package/dist/plugins/visual-diff/index.mjs.map +1 -0
  46. package/docs/plugins.md +415 -0
  47. package/package.json +40 -2
  48. package/skills/agent-lens/SKILL.md +142 -37
  49. package/skills/agent-lens/examples/06-state-testing-with-mock-ipc.md +13 -12
  50. package/skills/agent-lens/examples/07-live-controller-interactive-loop.md +108 -0
  51. package/skills/agent-lens/examples/08-accessibility-semantic-inspection.md +80 -0
  52. package/skills/agent-lens/examples/09-visual-regression-and-pixel-diffing.md +66 -0
  53. package/skills/agent-lens/examples/10-authoring-custom-agent-plugins.md +85 -0
  54. package/skills/agent-lens/references/plugin-development.md +165 -0
package/dist/cli.mjs CHANGED
@@ -1,18 +1,119 @@
1
- #!/usr/bin/env node
2
1
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
- }) : x)(function(x) {
6
- if (typeof require !== "undefined") return require.apply(this, arguments);
7
- throw Error('Dynamic require of "' + x + '" is not supported');
8
- });
9
2
  var __esm = (fn, res) => function __init() {
10
3
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
4
  };
12
- var __commonJS = (cb, mod) => function __require2() {
5
+ var __commonJS = (cb, mod) => function __require() {
13
6
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
7
  };
15
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
+
16
117
  // src/shared/api/dsl.ts
17
118
  function defineVisualTest(scenario) {
18
119
  return scenario;
@@ -21,10 +122,13 @@ var VIEWPORT_PRESETS;
21
122
  var init_dsl = __esm({
22
123
  "src/shared/api/dsl.ts"() {
23
124
  "use strict";
125
+ init_plugin();
126
+ init_console();
127
+ init_ipc();
24
128
  VIEWPORT_PRESETS = {
25
- /** Minimum supported window size (e.g., ) */
129
+ /** Minimum supported window size */
26
130
  MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
27
- /** Standard default window size (e.g., ) */
131
+ /** Standard default window size */
28
132
  DEFAULT: { name: "default", width: 1200, height: 800 },
29
133
  /** Wide screen for checking grids and tables */
30
134
  WIDE: { name: "wide", width: 1600, height: 900 },
@@ -35,8 +139,8 @@ var init_dsl = __esm({
35
139
  });
36
140
 
37
141
  // src/features/capture/capture.ts
38
- import fs from "fs";
39
- import path from "path";
142
+ import fs2 from "fs";
143
+ import path2 from "path";
40
144
  var CaptureEngine;
41
145
  var init_capture = __esm({
42
146
  "src/features/capture/capture.ts"() {
@@ -47,8 +151,8 @@ var init_capture = __esm({
47
151
  recordedSnapshots = [];
48
152
  constructor(outputDir) {
49
153
  this.outputDir = outputDir;
50
- if (!fs.existsSync(this.outputDir)) {
51
- fs.mkdirSync(this.outputDir, { recursive: true });
154
+ if (!fs2.existsSync(this.outputDir)) {
155
+ fs2.mkdirSync(this.outputDir, { recursive: true });
52
156
  }
53
157
  }
54
158
  getSnapshots() {
@@ -64,7 +168,7 @@ var init_capture = __esm({
64
168
  const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
65
169
  const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
66
170
  const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
67
- const filePath = path.join(this.outputDir, fileName);
171
+ const filePath = path2.join(this.outputDir, fileName);
68
172
  if (options?.selector) {
69
173
  const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
70
174
  await element.screenshot({ path: filePath });
@@ -100,7 +204,7 @@ var init_capture = __esm({
100
204
  const elapsedMs = frame * interval;
101
205
  const paddedFrame = String(frame + 1).padStart(2, "0");
102
206
  const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
103
- const filePath = path.join(this.outputDir, fileName);
207
+ const filePath = path2.join(this.outputDir, fileName);
104
208
  if (options.selector) {
105
209
  const element = await page.$(options.selector);
106
210
  if (element) {
@@ -137,8 +241,8 @@ var init_capture = __esm({
137
241
  });
138
242
 
139
243
  // src/features/reporter/reporter.ts
140
- import fs2 from "fs";
141
- import path2 from "path";
244
+ import fs3 from "fs";
245
+ import path3 from "path";
142
246
  var VisualReporter;
143
247
  var init_reporter = __esm({
144
248
  "src/features/reporter/reporter.ts"() {
@@ -154,8 +258,8 @@ var init_reporter = __esm({
154
258
  targetMode,
155
259
  durationMs
156
260
  } = data;
157
- const manifestPath = path2.join(outputDir, "manifest.json");
158
- const reportPath = path2.join(outputDir, "report.md");
261
+ const manifestPath = path3.join(outputDir, "manifest.json");
262
+ const reportPath = path3.join(outputDir, "report.md");
159
263
  const manifest = {
160
264
  scenarioId: scenario.id,
161
265
  title: scenario.title,
@@ -170,7 +274,7 @@ var init_reporter = __esm({
170
274
  errors: consoleErrors,
171
275
  warnings: consoleWarnings
172
276
  };
173
- fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
277
+ fs3.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
174
278
  const rows = [];
175
279
  const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
176
280
  const burstGroups = /* @__PURE__ */ new Map();
@@ -180,9 +284,9 @@ var init_reporter = __esm({
180
284
  burstGroups.set(s.burstGroup, list);
181
285
  });
182
286
  for (const snap of regularSnapshots) {
183
- const fileUri = `file:///${snap.filePath.replace(/\\/g, "/")}`;
287
+ const relativeLink = snap.relativeUri || `./${snap.fileName}`;
184
288
  rows.push(
185
- `| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${fileUri}) |`
289
+ `| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${relativeLink}) |`
186
290
  );
187
291
  }
188
292
  let burstSections = "";
@@ -199,8 +303,8 @@ var init_reporter = __esm({
199
303
  | :--- | :--- | :--- | :--- |
200
304
  `;
201
305
  for (const frame of frames) {
202
- const fileUri = `file:///${frame.filePath.replace(/\\/g, "/")}`;
203
- burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${fileUri}) | ![](${fileUri}) |
306
+ const relativeLink = frame.relativeUri || `./${frame.fileName}`;
307
+ burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${relativeLink}) | ![](${relativeLink}) |
204
308
  `;
205
309
  }
206
310
  burstSections += `
@@ -263,6 +367,17 @@ ${err.stack}
263
367
  if (consoleWarnings.length > 20) {
264
368
  consoleSections += `
265
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
+
266
381
  `;
267
382
  }
268
383
  }
@@ -285,18 +400,21 @@ ${rows.join("\n")}
285
400
 
286
401
  ${burstSections}
287
402
  ${consoleSections}
403
+ ${pluginSections}
288
404
  ---
289
405
 
290
406
  ## \u{1F4CB} AI Agent Verification Checklist:
291
407
  - [ ] **Console Errors**: ${consoleErrors.length === 0 ? "\u2705 No errors found" : `\u274C ${consoleErrors.length} errors \u2014 MUST REVIEW`}
408
+ - [ ] **Visual Layout Check**: Inspect snapshots (e.g. view_image on \`./01_${regularSnapshots[0]?.fileName || "quick_snap"}\`) for layout shifts or clipped elements.
292
409
  - [ ] **Responsiveness at \`1024x768\`**: Elements do not overflow the screen, no unwanted horizontal scroll.
410
+
293
411
  - [ ] **Typography & Spacing**: Spacing matches the design system and layout grids.
294
412
  - [ ] **Color Palette & Theme**: Background tints and button accent colors match the concept.
295
413
  - [ ] **Component States**: Modals open centered, dropdowns do not overlap with other layers (z-index).
296
414
  - [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
297
415
 
298
416
  `;
299
- fs2.writeFileSync(reportPath, reportContent, "utf-8");
417
+ fs3.writeFileSync(reportPath, reportContent, "utf-8");
300
418
  return reportPath;
301
419
  }
302
420
  static getHealthStatus(errors, warnings) {
@@ -320,8 +438,6 @@ var init_consoleTracker = __esm({
320
438
  ConsoleTracker = class {
321
439
  entries = [];
322
440
  attached = false;
323
- /**
324
- */
325
441
  attach(page) {
326
442
  if (this.attached) return;
327
443
  this.attached = true;
@@ -390,8 +506,6 @@ var init_consoleTracker = __esm({
390
506
  return "log";
391
507
  }
392
508
  }
393
- /**
394
- */
395
509
  isIgnoredWarning(text) {
396
510
  const ignoredPatterns = [
397
511
  "findDOMNode is deprecated",
@@ -411,394 +525,42 @@ var init_consoleTracker = __esm({
411
525
  });
412
526
 
413
527
  // src/shared/lib/playwrightLoader.ts
414
- import { chromium } from "playwright";
528
+ import { chromium as baseChromium } from "playwright";
529
+ var chromium;
415
530
  var init_playwrightLoader = __esm({
416
531
  "src/shared/lib/playwrightLoader.ts"() {
417
532
  "use strict";
418
- }
419
- });
420
-
421
- // src/shared/drivers/desktopDriver.ts
422
- import { spawn } from "child_process";
423
- import http from "http";
424
- import path3 from "path";
425
- import fs3 from "fs";
426
- var DesktopDriver;
427
- var init_desktopDriver = __esm({
428
- "src/shared/drivers/desktopDriver.ts"() {
429
- "use strict";
430
- init_playwrightLoader();
431
- DesktopDriver = class {
432
- port;
433
- executablePath;
434
- autoLaunch;
435
- args;
436
- env;
437
- cwd;
438
- childProcess = null;
439
- browser = null;
440
- context = null;
441
- page = null;
442
- processStderr = "";
443
- processExited = false;
444
- exitCode = null;
445
- constructor(options) {
446
- this.port = options?.port || 9222;
447
- this.autoLaunch = options?.autoLaunch ?? true;
448
- this.executablePath = options?.executablePath;
449
- this.args = options?.args || [];
450
- this.env = options?.env || {};
451
- this.cwd = options?.cwd;
452
- }
453
- async isPortAvailable() {
454
- return new Promise((resolve) => {
455
- const req = http.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
456
- resolve(res.statusCode === 200);
457
- });
458
- req.on("error", () => resolve(false));
459
- req.setTimeout(800, () => {
460
- req.destroy();
461
- resolve(false);
462
- });
463
- });
464
- }
465
- async waitForPort(timeoutMs = 25e3) {
466
- const startTime = Date.now();
467
- while (Date.now() - startTime < timeoutMs) {
468
- if (this.processExited) {
469
- throw new Error(
470
- `[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
471
- Stderr: ${this.processStderr.trim() || "(none)"}`
472
- );
473
- }
474
- if (await this.isPortAvailable()) {
475
- return;
476
- }
477
- await new Promise((r) => setTimeout(r, 400));
478
- }
479
- throw new Error(
480
- `Timeout waiting for WebView2/Chromium CDP port ${this.port}. Last stderr:
481
- ${this.processStderr.trim() || "(no stderr output)"}`
482
- );
483
- }
484
- async start(initialViewport = { width: 1200, height: 800 }) {
485
- const alreadyRunning = await this.isPortAvailable();
486
- if (!alreadyRunning) {
487
- if (!this.autoLaunch) {
488
- throw new Error(
489
- `App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
490
- );
491
- }
492
- if (!this.executablePath) {
493
- throw new Error(
494
- `[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
495
- );
496
- }
497
- const resolvedExe = path3.resolve(process.cwd(), this.executablePath);
498
- if (!fs3.existsSync(resolvedExe)) {
499
- throw new Error(
500
- `[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
501
- );
502
- }
503
- console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
504
- const mergedEnv = {
505
- ...process.env,
506
- ...this.env,
507
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${this.port}`
508
- };
509
- this.processStderr = "";
510
- this.processExited = false;
511
- this.exitCode = null;
512
- this.childProcess = spawn(resolvedExe, this.args, {
513
- env: mergedEnv,
514
- cwd: this.cwd || path3.dirname(resolvedExe),
515
- stdio: ["ignore", "ignore", "pipe"],
516
- detached: false
517
- });
518
- this.childProcess.stderr?.on("data", (chunk) => {
519
- this.processStderr += chunk.toString();
520
- });
521
- this.childProcess.on("exit", (code) => {
522
- this.processExited = true;
523
- this.exitCode = code;
524
- });
525
- this.childProcess.on("error", (err) => {
526
- console.error("[DesktopDriver] Failed to spawn process:", err);
527
- });
528
- console.log(`[DesktopDriver] Waiting for CDP debugging port on ${this.port}...`);
529
- await this.waitForPort();
530
- } else {
531
- console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
532
- }
533
- console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
534
- this.browser = await chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
535
- const contexts = this.browser.contexts();
536
- this.context = contexts[0] || await this.browser.newContext();
537
- const pages = this.context.pages();
538
- if (pages.length > 0) {
539
- this.page = pages[0];
540
- } else {
541
- this.page = await this.context.waitForEvent("page", { timeout: 1e4 });
542
- }
543
- if (initialViewport) {
544
- await this.page.setViewportSize(initialViewport).catch(() => {
545
- });
546
- }
547
- return { page: this.page, context: this.context, browser: this.browser };
548
- }
549
- async stop() {
550
- if (this.browser) {
551
- await this.browser.close().catch(() => {
552
- });
553
- this.browser = null;
554
- }
555
- if (this.childProcess && !this.childProcess.killed) {
556
- console.log("[DesktopDriver] Terminating spawned desktop process...");
557
- try {
558
- if (process.platform === "win32" && this.childProcess.pid) {
559
- spawn("taskkill", ["/pid", String(this.childProcess.pid), "/T", "/F"], { stdio: "ignore" });
560
- } else {
561
- this.childProcess.kill("SIGTERM");
562
- }
563
- } catch {
564
- }
565
- this.childProcess = null;
566
- }
567
- }
568
- };
569
- }
570
- });
571
-
572
- // src/features/mock-ipc/mockIpc.ts
573
- function generateMockIpcScript(registry) {
574
- const mocksJson = JSON.stringify(registry.toSerializable());
575
- return `
576
- (() => {
577
- const __mockTable = ${mocksJson};
578
-
579
- window.__visualRunnerMocks = __mockTable;
580
-
581
- // Generic IPC mock bridge for modern web applications
582
- window.__mockIpc = {
583
- invoke: (action, payload) => {
584
- return new Promise((resolve, reject) => {
585
- const mock = window.__visualRunnerMocks[action];
586
-
587
- if (mock) {
588
- setTimeout(() => {
589
- if (mock.type === 'ERROR') {
590
- reject(new Error(mock.data));
591
- } else {
592
- resolve(mock.data);
593
- }
594
- }, mock.delayMs || 20);
595
- } else {
596
- console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
597
- setTimeout(() => resolve(null), 20);
533
+ chromium = new Proxy(baseChromium, {
534
+ get(target, prop, receiver) {
535
+ if (prop === "launch") {
536
+ return async (...args) => {
537
+ try {
538
+ return await target.launch(...args);
539
+ } catch (err) {
540
+ if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
541
+ console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
542
+ console.error("\u{1F449} Please install it by running: npx playwright install chromium\n");
543
+ }
544
+ throw err;
598
545
  }
599
- });
600
- }
601
- };
602
-
603
- // Legacy fallback for generic window.external
604
- if (!window.external) {
605
- window.external = {};
606
- }
607
-
608
- window.external.sendMessage = (msg) => {
609
- try {
610
- const parsed = JSON.parse(msg);
611
- const action = parsed.Action || parsed.action;
612
- const id = parsed.Id || parsed.id;
613
-
614
- const mock = window.__visualRunnerMocks[action];
615
-
616
- if (mock) {
617
- const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
618
- setTimeout(() => {
619
- const cb = window.__mockCallback;
620
- if (cb) cb(JSON.stringify(response));
621
- }, mock.delayMs || 20);
622
- } else {
623
- setTimeout(() => {
624
- const cb = window.__mockCallback;
625
- if (cb) cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
626
- }, 20);
627
- }
628
- } catch (e) {
629
- console.error('[Mock IPC] Failed to process legacy message:', e);
630
- }
631
- };
632
-
633
- window.external.receiveMessage = (callback) => {
634
- window.__mockCallback = callback;
635
- };
636
-
637
- console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys(window.__visualRunnerMocks).length, 'mocked actions');
638
- })();
639
- `;
640
- }
641
- var MockIpcRegistry;
642
- var init_mockIpc = __esm({
643
- "src/features/mock-ipc/mockIpc.ts"() {
644
- "use strict";
645
- MockIpcRegistry = class {
646
- mocks = /* @__PURE__ */ new Map();
647
- set(action, data, options) {
648
- this.mocks.set(action, {
649
- action,
650
- data,
651
- type: options?.type ?? "SUCCESS",
652
- delayMs: options?.delayMs ?? 20
653
- });
654
- }
655
- setBatch(entries) {
656
- for (const entry of entries) {
657
- this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
658
- }
659
- }
660
- remove(action) {
661
- this.mocks.delete(action);
662
- }
663
- clear() {
664
- this.mocks.clear();
665
- }
666
- get(action) {
667
- return this.mocks.get(action) ?? null;
668
- }
669
- toSerializable() {
670
- const result = {};
671
- for (const [action, entry] of this.mocks.entries()) {
672
- result[action] = {
673
- data: entry.data,
674
- type: entry.type ?? "SUCCESS",
675
- delayMs: entry.delayMs ?? 20
676
546
  };
677
547
  }
678
- return result;
679
- }
680
- get size() {
681
- return this.mocks.size;
548
+ const val = Reflect.get(target, prop, receiver);
549
+ return typeof val === "function" ? val.bind(target) : val;
682
550
  }
683
- };
551
+ });
684
552
  }
685
553
  });
686
554
 
687
- // src/shared/lib/config.ts
555
+ // src/shared/drivers/previewDriver.ts
556
+ import http from "http";
688
557
  import fs4 from "fs";
689
558
  import path4 from "path";
690
- function loadConfig(cwd = process.cwd()) {
691
- const configPath = path4.join(cwd, "agent-lens.json");
692
- if (fs4.existsSync(configPath)) {
693
- try {
694
- const raw = fs4.readFileSync(configPath, "utf8");
695
- return JSON.parse(raw);
696
- } catch (e) {
697
- console.warn(`\u26A0\uFE0F Warning: Failed to parse agent-lens.json:`, e);
698
- }
699
- }
700
- const pkgPath = path4.join(cwd, "package.json");
701
- if (fs4.existsSync(pkgPath)) {
702
- try {
703
- const raw = fs4.readFileSync(pkgPath, "utf8");
704
- const pkg = JSON.parse(raw);
705
- if (pkg.agentLens && typeof pkg.agentLens === "object") {
706
- return pkg.agentLens;
707
- }
708
- } catch {
709
- }
710
- }
711
- return {};
712
- }
713
- function resolveWwwrootDir(customPath, cwd = process.cwd()) {
714
- if (customPath) {
715
- return path4.resolve(cwd, customPath);
716
- }
717
- const standardDirs = [
718
- "dist",
719
- "build",
720
- "wwwroot",
721
- "Frontend/dist",
722
- "frontend/dist",
723
- "client/dist",
724
- "web/dist",
725
- "ui/dist"
726
- ];
727
- for (const rel of standardDirs) {
728
- const candidate = path4.resolve(cwd, rel);
729
- if (fs4.existsSync(candidate) && fs4.existsSync(path4.join(candidate, "index.html"))) {
730
- return candidate;
731
- }
732
- }
733
- const foundDeepWwwroot = findDeepIndexHtmlDir(cwd, 4);
734
- if (foundDeepWwwroot) {
735
- return foundDeepWwwroot;
736
- }
737
- for (const rel of standardDirs) {
738
- const candidate = path4.resolve(cwd, rel);
739
- if (fs4.existsSync(candidate)) {
740
- return candidate;
741
- }
742
- }
743
- return path4.resolve(cwd, "dist");
744
- }
745
- function detectStartCwd(customCwd, rootCwd = process.cwd()) {
746
- if (customCwd) {
747
- return path4.resolve(rootCwd, customCwd);
748
- }
749
- const subdirectories = ["Frontend", "frontend", "client", "web", "ui", "app"];
750
- for (const sub of subdirectories) {
751
- const subPkg = path4.join(rootCwd, sub, "package.json");
752
- if (fs4.existsSync(subPkg)) {
753
- try {
754
- const json = JSON.parse(fs4.readFileSync(subPkg, "utf8"));
755
- if (json.scripts && (json.scripts.dev || json.scripts.start || json.scripts.build)) {
756
- return path4.join(rootCwd, sub);
757
- }
758
- } catch {
759
- }
760
- }
761
- }
762
- return rootCwd;
763
- }
764
- function findDeepIndexHtmlDir(dir, maxDepth, currentDepth = 0) {
765
- if (currentDepth > maxDepth || !fs4.existsSync(dir)) return null;
766
- try {
767
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
768
- for (const entry of entries) {
769
- if (entry.isDirectory()) {
770
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "artifacts") {
771
- continue;
772
- }
773
- const subDir = path4.join(dir, entry.name);
774
- if (entry.name === "wwwroot" && fs4.existsSync(path4.join(subDir, "index.html"))) {
775
- return subDir;
776
- }
777
- const found = findDeepIndexHtmlDir(subDir, maxDepth, currentDepth + 1);
778
- if (found) return found;
779
- }
780
- }
781
- } catch {
782
- return null;
783
- }
784
- return null;
785
- }
786
- var init_config = __esm({
787
- "src/shared/lib/config.ts"() {
788
- "use strict";
789
- }
790
- });
791
-
792
- // src/shared/drivers/previewDriver.ts
793
- import http2 from "http";
794
- import fs5 from "fs";
795
- import path5 from "path";
796
559
  var MIME_TYPES, PreviewDriver;
797
560
  var init_previewDriver = __esm({
798
561
  "src/shared/drivers/previewDriver.ts"() {
799
562
  "use strict";
800
563
  init_playwrightLoader();
801
- init_mockIpc();
802
564
  init_config();
803
565
  MIME_TYPES = {
804
566
  ".html": "text/html; charset=utf-8",
@@ -823,13 +585,12 @@ var init_previewDriver = __esm({
823
585
  serverPort = 0;
824
586
  options;
825
587
  _baseUrl = "";
826
- mockRegistry;
588
+ initialRouteMocks = [];
827
589
  constructor(options) {
828
590
  this.options = options || {};
829
591
  if (!this.options.url) {
830
592
  this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
831
593
  }
832
- this.mockRegistry = new MockIpcRegistry();
833
594
  }
834
595
  get baseUrl() {
835
596
  return this._baseUrl;
@@ -837,25 +598,25 @@ var init_previewDriver = __esm({
837
598
  async start(initialViewport = { width: 1200, height: 800 }) {
838
599
  let targetUrl = this.options.url;
839
600
  if (!targetUrl) {
840
- if (!fs5.existsSync(this.options.wwwrootDir)) {
601
+ if (!fs4.existsSync(this.options.wwwrootDir)) {
841
602
  throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
842
603
  }
843
604
  await new Promise((resolve, reject) => {
844
- this.server = http2.createServer((req, res) => {
605
+ this.server = http.createServer((req, res) => {
845
606
  let reqUrl = req.url?.split("?")[0] || "/";
846
607
  if (reqUrl === "/") reqUrl = "/index.html";
847
- let safePath = path5.normalize(path5.join(this.options.wwwrootDir, reqUrl));
608
+ let safePath = path4.normalize(path4.join(this.options.wwwrootDir, reqUrl));
848
609
  if (!safePath.startsWith(this.options.wwwrootDir)) {
849
610
  res.writeHead(403);
850
611
  return res.end("Forbidden");
851
612
  }
852
- if (!fs5.existsSync(safePath) || fs5.statSync(safePath).isDirectory()) {
853
- safePath = path5.join(this.options.wwwrootDir, "index.html");
613
+ if (!fs4.existsSync(safePath) || fs4.statSync(safePath).isDirectory()) {
614
+ safePath = path4.join(this.options.wwwrootDir, "index.html");
854
615
  }
855
- const ext = path5.extname(safePath).toLowerCase();
616
+ const ext = path4.extname(safePath).toLowerCase();
856
617
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
857
618
  try {
858
- const content = fs5.readFileSync(safePath);
619
+ const content = fs4.readFileSync(safePath);
859
620
  res.writeHead(200, { "Content-Type": contentType });
860
621
  res.end(content);
861
622
  } catch (e) {
@@ -887,35 +648,41 @@ var init_previewDriver = __esm({
887
648
  viewport: initialViewport,
888
649
  deviceScaleFactor: 1
889
650
  });
890
- if (this.mockRegistry.size > 0) {
891
- const mockScript = generateMockIpcScript(this.mockRegistry);
892
- await this.context.addInitScript(mockScript);
893
- }
894
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
+ }
895
657
  await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
896
658
  return { page: this.page, context: this.context, browser: this.browser };
897
659
  }
898
- async updateMockIpc(action, data, options) {
660
+ async addRouteMock(entry) {
899
661
  if (!this.page) {
900
- throw new Error("PreviewDriver not started. Call start() first.");
662
+ this.initialRouteMocks.push(entry);
663
+ return;
901
664
  }
902
- this.mockRegistry.set(action, data, options);
903
- await this.page.evaluate(
904
- ({ action: action2, mock }) => {
905
- if (!window.__visualRunnerMocks) {
906
- window.__visualRunnerMocks = {};
907
- }
908
- window.__visualRunnerMocks[action2] = mock;
909
- },
910
- {
911
- action,
912
- mock: {
913
- data,
914
- type: options?.type ?? "SUCCESS",
915
- delayMs: options?.delayMs ?? 20
916
- }
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));
917
672
  }
918
- );
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
679
+ });
680
+ });
681
+ }
682
+ async setupRouteMocks(routes) {
683
+ for (const r of routes) {
684
+ await this.addRouteMock(r);
685
+ }
919
686
  }
920
687
  async stop() {
921
688
  if (this.context) {
@@ -940,141 +707,540 @@ var init_previewDriver = __esm({
940
707
  });
941
708
 
942
709
  // src/shared/lib/processManager.ts
943
- import { spawn as spawn2 } from "child_process";
710
+ import { spawn } from "child_process";
944
711
  import treeKill from "tree-kill";
945
712
  var ProcessManager;
946
713
  var init_processManager = __esm({
947
714
  "src/shared/lib/processManager.ts"() {
948
715
  "use strict";
949
716
  ProcessManager = class {
950
- child = null;
951
- stderrOutput = "";
952
- stdoutOutput = "";
953
- hasExited = false;
954
- exitCode = null;
955
- /**
956
- * Spawns a background process (e.g. dev server, backend, or app)
957
- */
717
+ childProcess = null;
718
+ isStopped = false;
958
719
  async start(command, options) {
959
- this.stderrOutput = "";
960
- this.stdoutOutput = "";
961
- this.hasExited = false;
962
- this.exitCode = null;
963
720
  const cwd = options?.cwd || process.cwd();
964
- const env = { ...process.env, ...options?.env };
965
721
  console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
966
- this.child = spawn2(command, {
722
+ this.childProcess = spawn(command, {
967
723
  cwd,
968
- env,
969
- shell: options?.shell ?? true,
724
+ env: { ...process.env, ...options?.env },
725
+ shell: true,
970
726
  stdio: ["ignore", "pipe", "pipe"]
971
727
  });
972
- this.child.stdout?.on("data", (chunk) => {
973
- const str = chunk.toString();
974
- this.stdoutOutput += str;
975
- });
976
- this.child.stderr?.on("data", (chunk) => {
977
- const str = chunk.toString();
978
- this.stderrOutput += str;
728
+ this.childProcess.stdout?.on("data", (chunk) => {
729
+ const line = chunk.toString().trim();
730
+ if (line) {
731
+ }
979
732
  });
980
- this.child.on("exit", (code) => {
981
- this.hasExited = true;
982
- this.exitCode = code;
733
+ this.childProcess.stderr?.on("data", (chunk) => {
734
+ const line = chunk.toString().trim();
735
+ if (line && !line.includes("ExperimentalWarning")) {
736
+ }
983
737
  });
984
- this.child.on("error", (err) => {
985
- console.error(`\u274C [ProcessManager] Failed to start command: "${command}":`, err.message);
738
+ this.childProcess.on("exit", (code, signal) => {
739
+ if (!this.isStopped && code !== 0 && code !== null) {
740
+ console.warn(`\u26A0\uFE0F [ProcessManager] Subprocess exited prematurely with code ${code}, signal ${signal}`);
741
+ }
986
742
  });
987
743
  }
988
- /**
989
- * Polls a URL until it starts responding or until timeout is reached.
990
- */
991
744
  async waitForUrl(url, timeoutMs = 3e4) {
745
+ console.log(`\u23F3 [ProcessManager] Waiting for ${url} to respond...`);
992
746
  const startTime = Date.now();
993
- console.log(`\u23F3 [ProcessManager] Waiting for URL to become available: ${url} (timeout: ${timeoutMs / 1e3}s)...`);
994
747
  while (Date.now() - startTime < timeoutMs) {
995
- if (this.hasExited && this.exitCode !== 0) {
748
+ if (this.childProcess && this.childProcess.exitCode !== null) {
996
749
  throw new Error(
997
- `[ProcessManager] Process exited prematurely with code ${this.exitCode}.
998
- Stderr:
999
- ${this.stderrOutput.trim() || "(no stderr)"}
1000
- Stdout:
1001
- ${this.stdoutOutput.slice(-500).trim()}`
750
+ `[ProcessManager] Server process exited with code ${this.childProcess.exitCode} while waiting for ${url}`
1002
751
  );
1003
752
  }
1004
753
  try {
1005
- const response = await fetch(url, { method: "GET", signal: AbortSignal.timeout(2e3) });
754
+ const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
1006
755
  if (response.status) {
1007
- console.log(`\u2705 [ProcessManager] Server responded with status ${response.status} at ${url}`);
1008
- return;
756
+ console.log(`\u2705 [ProcessManager] Target ${url} is ready (status: ${response.status})!`);
757
+ return true;
1009
758
  }
1010
759
  } catch {
1011
760
  }
1012
- await new Promise((resolve) => setTimeout(resolve, 350));
761
+ await new Promise((r) => setTimeout(r, 500));
1013
762
  }
1014
- throw new Error(
1015
- `[ProcessManager] Timeout (${timeoutMs / 1e3}s) waiting for server at ${url}.
1016
- Last stdout:
1017
- ${this.stdoutOutput.slice(-500).trim()}
1018
- Last stderr:
1019
- ${this.stderrOutput.trim()}`
1020
- );
763
+ throw new Error(`[ProcessManager] Timeout after ${timeoutMs}ms waiting for ${url} to respond.`);
1021
764
  }
1022
- /**
1023
- * Gracefully and forcefully kills the process and all its children.
1024
- */
1025
765
  async stop() {
1026
- if (!this.child || !this.child.pid || this.hasExited) {
1027
- this.child = null;
766
+ if (this.isStopped || !this.childProcess || !this.childProcess.pid) {
1028
767
  return;
1029
768
  }
1030
- const pid = this.child.pid;
1031
- console.log(`\u{1F6D1} [ProcessManager] Terminating process tree (PID: ${pid})...`);
769
+ this.isStopped = true;
770
+ const pid = this.childProcess.pid;
771
+ console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
1032
772
  await new Promise((resolve) => {
1033
- treeKill(pid, "SIGKILL", (err) => {
773
+ treeKill(pid, "SIGTERM", (err) => {
1034
774
  if (err) {
1035
- if (process.platform === "win32") {
1036
- try {
1037
- spawn2("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1038
- } catch {
1039
- }
775
+ try {
776
+ treeKill(pid, "SIGKILL");
777
+ } catch {
1040
778
  }
1041
779
  }
1042
780
  resolve();
1043
781
  });
1044
782
  });
1045
- this.child = null;
783
+ this.childProcess = null;
1046
784
  }
1047
785
  };
1048
786
  }
1049
787
  });
1050
788
 
1051
- // src/features/runner/runner.ts
1052
- import path6 from "path";
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
1053
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";
1202
+ }
1203
+ });
1204
+
1205
+ // src/features/runner/runner.ts
1206
+ import path7 from "path";
1207
+ import fs7 from "fs";
1054
1208
  async function runVisualScenario(options) {
1055
1209
  const { scenario } = options;
1056
1210
  const targetMode = options.targetMode || "preview";
1057
1211
  const startTime = Date.now();
1058
1212
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1059
- const artifactsRoot = options.artifactsRoot || path6.resolve(process.cwd(), "artifacts");
1060
- if (options.cleanArtifacts && fs6.existsSync(artifactsRoot)) {
1213
+ const artifactsRoot = options.artifactsRoot || path7.resolve(process.cwd(), "artifacts");
1214
+ if (options.cleanArtifacts && fs7.existsSync(artifactsRoot)) {
1061
1215
  console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
1062
- const entries = fs6.readdirSync(artifactsRoot, { withFileTypes: true });
1216
+ const entries = fs7.readdirSync(artifactsRoot, { withFileTypes: true });
1063
1217
  for (const entry of entries) {
1064
- fs6.rmSync(path6.join(artifactsRoot, entry.name), { recursive: true, force: true });
1218
+ fs7.rmSync(path7.join(artifactsRoot, entry.name), { recursive: true, force: true });
1065
1219
  }
1066
1220
  }
1067
- const scenarioArtifactsDir = path6.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1068
- if (!fs6.existsSync(scenarioArtifactsDir)) {
1069
- 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 });
1070
1224
  }
1071
1225
  const captureEngine = new CaptureEngine(scenarioArtifactsDir);
1072
1226
  const consoleTracker = new ConsoleTracker();
1073
1227
  const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
1074
- let currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1075
- let desktopDriver = null;
1228
+ const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1076
1229
  let previewDriver = null;
1077
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
+ };
1078
1244
  try {
1079
1245
  let page;
1080
1246
  let context;
@@ -1091,205 +1257,63 @@ async function runVisualScenario(options) {
1091
1257
  const waitTarget = options.url || "http://localhost:5173";
1092
1258
  await processManager.waitForUrl(waitTarget);
1093
1259
  }
1260
+ await pluginManager.runSetup(hookContext);
1094
1261
  if (typeof scenario.setup === "function") {
1095
1262
  console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
1096
1263
  await scenario.setup();
1097
1264
  }
1098
- if (targetMode === "desktop") {
1099
- desktopDriver = new DesktopDriver({
1100
- port: options.port || 9222,
1101
- autoLaunch: options.autoLaunchDesktop ?? true,
1102
- executablePath: options.executablePath,
1103
- args: options.desktopArgs,
1104
- env: options.desktopEnv
1105
- });
1106
- const res = await desktopDriver.start(currentViewport);
1107
- page = res.page;
1108
- 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
+ );
1109
1279
  } else {
1110
1280
  previewDriver = new PreviewDriver({
1111
1281
  wwwrootDir: options.wwwrootDir,
1112
1282
  url: options.url,
1113
1283
  headed: options.headed
1114
1284
  });
1115
- const mergedMocks = [...options.globalMocks || [], ...scenario.mockIpc || []];
1116
- if (mergedMocks.length > 0) {
1117
- console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
1118
- previewDriver.mockRegistry.setBatch(mergedMocks);
1285
+ if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1286
+ console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
1287
+ await previewDriver.setupRouteMocks(scenario.mockRoutes);
1119
1288
  }
1120
1289
  const res = await previewDriver.start(currentViewport);
1121
1290
  page = res.page;
1122
1291
  context = res.context;
1292
+ if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1293
+ await previewDriver.setupRouteMocks(scenario.mockRoutes);
1294
+ }
1123
1295
  }
1296
+ await pluginManager.runOnContextCreated(context, hookContext);
1297
+ await pluginManager.runOnPageCreated(page, context, hookContext);
1124
1298
  consoleTracker.attach(page);
1125
1299
  console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
1126
1300
  `);
1127
- const doNavigate = async (route) => {
1128
- if (route.startsWith("http://") || route.startsWith("https://")) {
1129
- await page.goto(route, { waitUntil: "domcontentloaded" });
1130
- await page.waitForTimeout(300);
1131
- return;
1132
- }
1133
- if (route.startsWith("#")) {
1134
- await page.evaluate((r) => {
1135
- window.location.hash = r;
1136
- }, route);
1137
- await page.waitForTimeout(300);
1138
- return;
1139
- }
1140
- if (previewDriver?.baseUrl && options.url) {
1141
- try {
1142
- const fullUrl = new URL(route, previewDriver.baseUrl).toString();
1143
- await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
1144
- await page.waitForTimeout(300);
1145
- return;
1146
- } catch {
1147
- }
1148
- }
1149
- const routePath = route.startsWith("/") ? route : `/${route}`;
1150
- await page.evaluate((r) => {
1151
- if (window.location.hash !== void 0) {
1152
- window.location.hash = r;
1153
- }
1154
- }, routePath);
1155
- await page.waitForTimeout(300);
1156
- };
1301
+ const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
1157
1302
  if (scenario.route) {
1158
- console.log(`[Runner] Navigating to route: ${scenario.route}`);
1303
+ console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
1159
1304
  await doNavigate(scenario.route);
1160
1305
  }
1161
- const ctx = {
1306
+ const { ctx } = buildTestContext({
1162
1307
  page,
1163
1308
  context,
1164
1309
  targetMode,
1165
- currentViewport,
1166
- capture: async (name, opts) => {
1167
- console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
1168
- return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
1169
- },
1170
- captureBurst: async (name, opts) => {
1171
- console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
1172
- return await captureEngine.takeBurst(page, name, currentViewport, opts);
1173
- },
1174
- navigate: async (route) => {
1175
- console.log(`\u{1F9ED} [Navigate] ${route}`);
1176
- await doNavigate(route);
1177
- },
1178
- // ─── Viewport ───
1179
- resize: async (width, height) => {
1180
- console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1181
- currentViewport = { width, height };
1182
- ctx.currentViewport = currentViewport;
1183
- await page.setViewportSize(currentViewport);
1184
- await page.waitForTimeout(200);
1185
- },
1186
- setPreset: async (preset) => {
1187
- console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1188
- await ctx.resize(preset.width, preset.height);
1189
- },
1190
- resizeToFit: async (selector, padding = 0) => {
1191
- let boundingBox;
1192
- if (selector) {
1193
- const el = await page.$(selector);
1194
- if (el) {
1195
- boundingBox = await el.boundingBox();
1196
- }
1197
- } else {
1198
- boundingBox = await page.evaluate(() => {
1199
- return {
1200
- width: document.documentElement.scrollWidth,
1201
- height: document.documentElement.scrollHeight
1202
- };
1203
- });
1204
- }
1205
- if (boundingBox) {
1206
- const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1207
- const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1208
- console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1209
- await ctx.resize(newWidth, newHeight);
1210
- } else {
1211
- console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1212
- }
1213
- },
1214
- wait: async (ms) => {
1215
- await page.waitForTimeout(ms);
1216
- },
1217
- waitForSelector: async (selector, timeoutMs = 5e3) => {
1218
- await page.waitForSelector(selector, { timeout: timeoutMs });
1219
- },
1220
- click: async (selector) => {
1221
- console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1222
- await page.click(selector);
1223
- },
1224
- rightClick: async (selector) => {
1225
- console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1226
- await page.click(selector, { button: "right" });
1227
- },
1228
- type: async (selector, text) => {
1229
- console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1230
- await page.fill(selector, text);
1231
- },
1232
- selectOption: async (selector, value) => {
1233
- console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1234
- await page.selectOption(selector, value);
1235
- },
1236
- hover: async (selector) => {
1237
- console.log(`\u{1F446} [Hover] ${selector}`);
1238
- await page.hover(selector);
1239
- },
1240
- scroll: async (selector, deltaY) => {
1241
- console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1242
- await page.evaluate(({ sel, dY }) => {
1243
- const el = document.querySelector(sel);
1244
- if (el) {
1245
- el.scrollTop += dY;
1246
- } else {
1247
- window.scrollBy(0, dY);
1248
- }
1249
- }, { sel: selector, dY: deltaY });
1250
- await page.waitForTimeout(100);
1251
- },
1252
- log: (msg) => {
1253
- console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1254
- },
1255
- // ─── Mock IPC ───
1256
- setMockIpc: async (action, data, mockOptions) => {
1257
- if (targetMode === "desktop") {
1258
- console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
1259
- return;
1260
- }
1261
- if (!previewDriver) {
1262
- console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
1263
- return;
1264
- }
1265
- console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
1266
- await previewDriver.updateMockIpc(action, data, mockOptions);
1267
- },
1268
- getConsoleErrors: () => consoleTracker.getErrors(),
1269
- getConsoleWarnings: () => consoleTracker.getWarnings(),
1270
- hasConsoleErrors: () => consoleTracker.hasErrors,
1271
- // ─── DOM Assertions ───
1272
- readText: async (selector) => {
1273
- const text = await page.textContent(selector);
1274
- return text ? text.trim() : null;
1275
- },
1276
- getPageText: async () => {
1277
- return await page.evaluate(() => document.body.innerText || "");
1278
- },
1279
- isVisible: async (selector) => {
1280
- try {
1281
- const element = await page.$(selector);
1282
- if (!element) return false;
1283
- return await element.isVisible();
1284
- } catch {
1285
- return false;
1286
- }
1287
- },
1288
- getElementCount: async (selector) => {
1289
- const elements = await page.$$(selector);
1290
- return elements.length;
1291
- }
1292
- };
1310
+ initialViewport: currentViewport,
1311
+ captureEngine,
1312
+ consoleTracker,
1313
+ previewDriver,
1314
+ doNavigate
1315
+ });
1316
+ await pluginManager.extendContext(ctx, page, hookContext);
1293
1317
  await scenario.run(ctx);
1294
1318
  const durationMs = Date.now() - startTime;
1295
1319
  const snapshots = captureEngine.getSnapshots();
@@ -1304,38 +1328,19 @@ async function runVisualScenario(options) {
1304
1328
  console.log(`
1305
1329
  \u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
1306
1330
  }
1307
- const reportPath = VisualReporter.generateReport({
1331
+ const reportData = {
1308
1332
  scenario,
1309
1333
  snapshots,
1310
1334
  consoleErrors,
1311
1335
  consoleWarnings,
1312
1336
  outputDir: scenarioArtifactsDir,
1313
1337
  targetMode,
1314
- durationMs
1315
- });
1316
- const syncLatest = (repPath, snaps) => {
1317
- try {
1318
- const latestDir = path6.join(artifactsRoot, "latest");
1319
- if (fs6.existsSync(latestDir)) {
1320
- fs6.rmSync(latestDir, { recursive: true, force: true });
1321
- }
1322
- fs6.mkdirSync(latestDir, { recursive: true });
1323
- fs6.copyFileSync(repPath, path6.join(latestDir, "report.md"));
1324
- const manifestSrc = path6.join(scenarioArtifactsDir, "manifest.json");
1325
- if (fs6.existsSync(manifestSrc)) {
1326
- fs6.copyFileSync(manifestSrc, path6.join(latestDir, "manifest.json"));
1327
- }
1328
- for (const snap of snaps) {
1329
- if (snap.filePath && fs6.existsSync(snap.filePath)) {
1330
- fs6.copyFileSync(snap.filePath, path6.join(latestDir, snap.fileName));
1331
- }
1332
- }
1333
- return path6.join(latestDir, "report.md");
1334
- } catch {
1335
- return void 0;
1336
- }
1338
+ durationMs,
1339
+ customSections: []
1337
1340
  };
1338
- 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);
1339
1344
  console.log(`
1340
1345
  \u2705 Visual Test Completed Successfully!`);
1341
1346
  console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
@@ -1362,7 +1367,7 @@ async function runVisualScenario(options) {
1362
1367
  const snapshots = captureEngine.getSnapshots();
1363
1368
  const consoleErrors = consoleTracker.getErrors();
1364
1369
  const consoleWarnings = consoleTracker.getWarnings();
1365
- const reportPath = VisualReporter.generateReport({
1370
+ const reportData = {
1366
1371
  scenario,
1367
1372
  snapshots,
1368
1373
  consoleErrors,
@@ -1370,16 +1375,9 @@ async function runVisualScenario(options) {
1370
1375
  outputDir: scenarioArtifactsDir,
1371
1376
  targetMode,
1372
1377
  durationMs
1373
- });
1374
- try {
1375
- const latestDir = path6.join(artifactsRoot, "latest");
1376
- if (fs6.existsSync(latestDir)) {
1377
- fs6.rmSync(latestDir, { recursive: true, force: true });
1378
- }
1379
- fs6.mkdirSync(latestDir, { recursive: true });
1380
- fs6.copyFileSync(reportPath, path6.join(latestDir, "report.md"));
1381
- } catch {
1382
- }
1378
+ };
1379
+ const reportPath = VisualReporter.generateReport(reportData);
1380
+ syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1383
1381
  return {
1384
1382
  scenarioId: scenario.id,
1385
1383
  targetMode,
@@ -1392,6 +1390,7 @@ async function runVisualScenario(options) {
1392
1390
  error: err.message
1393
1391
  };
1394
1392
  } finally {
1393
+ await pluginManager.runTeardown(hookContext);
1395
1394
  if (typeof scenario.teardown === "function") {
1396
1395
  try {
1397
1396
  console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
@@ -1403,10 +1402,10 @@ async function runVisualScenario(options) {
1403
1402
  if (options.cleanPaths && options.cleanPaths.length > 0) {
1404
1403
  for (const cleanTarget of options.cleanPaths) {
1405
1404
  try {
1406
- const resolvedCleanPath = path6.resolve(process.cwd(), cleanTarget);
1407
- if (fs6.existsSync(resolvedCleanPath)) {
1405
+ const resolvedCleanPath = path7.resolve(process.cwd(), cleanTarget);
1406
+ if (fs7.existsSync(resolvedCleanPath)) {
1408
1407
  console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
1409
- fs6.rmSync(resolvedCleanPath, { recursive: true, force: true });
1408
+ fs7.rmSync(resolvedCleanPath, { recursive: true, force: true });
1410
1409
  }
1411
1410
  } catch (cleanErr) {
1412
1411
  console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
@@ -1414,7 +1413,7 @@ async function runVisualScenario(options) {
1414
1413
  }
1415
1414
  }
1416
1415
  if (!options.detach) {
1417
- if (desktopDriver) await desktopDriver.stop();
1416
+ if (customDriverStop) await customDriverStop();
1418
1417
  if (previewDriver) await previewDriver.stop();
1419
1418
  if (processManager) await processManager.stop();
1420
1419
  } else {
@@ -1429,15 +1428,29 @@ var init_runner = __esm({
1429
1428
  init_capture();
1430
1429
  init_reporter();
1431
1430
  init_consoleTracker();
1432
- init_desktopDriver();
1433
1431
  init_previewDriver();
1434
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();
1435
1448
  }
1436
1449
  });
1437
1450
 
1438
1451
  // src/features/snap/snap.ts
1439
- import path7 from "path";
1440
- import fs7 from "fs";
1452
+ import path8 from "path";
1453
+ import fs8 from "fs";
1441
1454
  function parseViewportPresets(raw) {
1442
1455
  if (!raw || raw.length === 0) {
1443
1456
  return [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
@@ -1464,26 +1477,33 @@ async function isPortResponding(url) {
1464
1477
  return false;
1465
1478
  }
1466
1479
  }
1480
+ async function detectActiveDevServer() {
1481
+ for (const port of CANDIDATE_PORTS) {
1482
+ const url = `http://localhost:${port}`;
1483
+ if (await isPortResponding(url)) {
1484
+ return url;
1485
+ }
1486
+ }
1487
+ return null;
1488
+ }
1467
1489
  async function runQuickSnap(options) {
1468
1490
  let targetUrl = options.url;
1469
1491
  let useStaticPreview = false;
1470
1492
  let wwwrootDir;
1471
1493
  if (!targetUrl && !options.start) {
1472
- if (await isPortResponding("http://localhost:5173")) {
1473
- targetUrl = "http://localhost:5173";
1474
- console.log(`\u{1F310} [Quick Snap] Detected active dev server on http://localhost:5173`);
1475
- } else if (await isPortResponding("http://localhost:3000")) {
1476
- targetUrl = "http://localhost:3000";
1477
- console.log(`\u{1F310} [Quick Snap] Detected active dev server on http://localhost:3000`);
1494
+ const activeUrl = await detectActiveDevServer();
1495
+ if (activeUrl) {
1496
+ targetUrl = activeUrl;
1497
+ console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
1478
1498
  } else {
1479
1499
  const candidateDir = resolveWwwrootDir();
1480
- if (fs7.existsSync(candidateDir) && fs7.existsSync(path7.join(candidateDir, "index.html"))) {
1500
+ if (fs8.existsSync(candidateDir) && fs8.existsSync(path8.join(candidateDir, "index.html"))) {
1481
1501
  useStaticPreview = true;
1482
1502
  wwwrootDir = candidateDir;
1483
1503
  console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
1484
1504
  } else {
1485
1505
  console.error(`
1486
- \u274C [Quick Snap] No active server found on http://localhost:5173 or :3000, and no static build found.`);
1506
+ \u274C [Quick Snap] No active server found on common ports (${CANDIDATE_PORTS.join(", ")}), and no static build found.`);
1487
1507
  console.log(`\u{1F4A1} Suggested actions:`);
1488
1508
  console.log(` 1. Pass a start command: npx agent-lens snap --start="npm run dev"`);
1489
1509
  console.log(` 2. Specify your URL: npx agent-lens snap --url=http://localhost:8080`);
@@ -1519,7 +1539,7 @@ async function runQuickSnap(options) {
1519
1539
  ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
1520
1540
  await ctx.setPreset(vp);
1521
1541
  await ctx.wait(200);
1522
- await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`);
1542
+ await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
1523
1543
  }
1524
1544
  if (options.selector) {
1525
1545
  ctx.log(`Focusing on selector: "${options.selector}"`);
@@ -1552,7 +1572,8 @@ async function runQuickSnap(options) {
1552
1572
  port: options.port,
1553
1573
  headed: options.headed,
1554
1574
  detach: options.detach,
1555
- 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
1556
1577
  });
1557
1578
  console.log(`
1558
1579
  ========================================`);
@@ -1565,7 +1586,7 @@ async function runQuickSnap(options) {
1565
1586
  `);
1566
1587
  return result.success && result.consoleErrors === 0;
1567
1588
  }
1568
- var PRESET_MAP;
1589
+ var PRESET_MAP, CANDIDATE_PORTS;
1569
1590
  var init_snap = __esm({
1570
1591
  "src/features/snap/snap.ts"() {
1571
1592
  "use strict";
@@ -1582,107 +1603,122 @@ var init_snap = __esm({
1582
1603
  tablet: { name: "tablet", width: 768, height: 1024 },
1583
1604
  "full-hd": VIEWPORT_PRESETS.FULL_HD
1584
1605
  };
1606
+ CANDIDATE_PORTS = [5173, 3e3, 4321, 4200, 8080, 8e3, 3001];
1585
1607
  }
1586
1608
  });
1587
1609
 
1588
- // src/app/cli.ts
1589
- import path8 from "path";
1590
- import fs8 from "fs";
1591
- import { createJiti } from "jiti";
1592
- var require_cli = __commonJS({
1593
- "src/app/cli.ts"() {
1594
- init_runner();
1595
- init_config();
1596
- init_snap();
1597
- var fileConfig = loadConfig();
1598
- var args = process.argv.slice(2);
1599
- if (args[0] === "init") {
1600
- initScenarioTemplate();
1601
- process.exit(0);
1602
- }
1603
- var isSnapCommand = args[0] === "snap";
1604
- var effectiveArgs = isSnapCommand ? args.slice(1) : args;
1605
- var options = {
1606
- mode: fileConfig.mode || "preview",
1607
- port: fileConfig.port || 9222,
1608
- headed: fileConfig.headed ?? false,
1609
- detach: fileConfig.detach ?? false,
1610
- build: fileConfig.buildCommand || false,
1611
- start: fileConfig.startCommand,
1612
- startCwd: fileConfig.startCwd,
1613
- cleanArtifacts: fileConfig.cleanArtifacts ?? false,
1614
- url: fileConfig.url,
1615
- exe: fileConfig.executablePath,
1616
- clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
1617
- dir: fileConfig.scenarios,
1618
- wwwroot: fileConfig.wwwroot,
1619
- outDir: fileConfig.outDir
1620
- };
1621
- for (const arg of effectiveArgs) {
1622
- if (arg === "--help" || arg === "-h") {
1623
- options.help = true;
1624
- } else if (arg.startsWith("--scenario=")) {
1625
- options.scenario = arg.split("=")[1];
1626
- } else if (arg === "--all") {
1627
- options.all = true;
1628
- } else if (arg.startsWith("--mode=")) {
1629
- const mode = arg.split("=")[1].toLowerCase();
1630
- if (mode === "desktop" || mode === "preview") {
1631
- options.mode = mode;
1632
- }
1633
- } else if (arg.startsWith("--port=")) {
1634
- options.port = parseInt(arg.split("=")[1], 10) || 9222;
1635
- } else if (arg === "--headed") {
1636
- options.headed = true;
1637
- } else if (arg === "--detach") {
1638
- options.detach = true;
1639
- } else if (arg === "--build") {
1640
- options.build = true;
1641
- } else if (arg.startsWith("--build=")) {
1642
- options.build = arg.slice("--build=".length);
1643
- } else if (arg.startsWith("--start=")) {
1644
- options.start = arg.slice("--start=".length);
1645
- } else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
1646
- options.startCwd = arg.split("=")[1];
1647
- } else if (arg === "--clean-artifacts") {
1648
- options.cleanArtifacts = true;
1649
- } else if (arg.startsWith("--url=")) {
1650
- options.url = arg.split("=")[1];
1651
- } else if (arg.startsWith("--selector=")) {
1652
- options.selector = arg.split("=")[1];
1653
- } else if (arg.startsWith("--viewports=")) {
1654
- options.viewports = arg.split("=")[1].split(",").map((v) => v.trim()).filter(Boolean);
1655
- } else if (arg.startsWith("--wait=")) {
1656
- options.waitMs = parseInt(arg.split("=")[1], 10);
1657
- } else if (arg.startsWith("--name=")) {
1658
- options.name = arg.split("=")[1];
1659
- } else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
1660
- options.exe = arg.split("=")[1];
1661
- } else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
1662
- const rawPaths = arg.split("=")[1];
1663
- options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
1664
- } else if (arg.startsWith("--dir=")) {
1665
- options.dir = arg.split("=")[1];
1666
- } else if (arg.startsWith("--wwwroot=")) {
1667
- options.wwwroot = arg.split("=")[1];
1668
- } else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
1669
- options.outDir = arg.split("=")[1];
1670
- }
1671
- }
1672
- if (options.start && !options.startCwd) {
1673
- 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;
1674
1687
  }
1675
- function printHelp() {
1676
- 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(`
1677
1711
  \u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
1678
1712
 
1679
1713
  Usage:
1680
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)
1681
1716
  npx agent-lens [options] Run scripted scenario tests from scenarios/
1682
1717
  npx agent-lens init Generate starter scenario template & mocks
1683
1718
 
1684
1719
  Commands:
1685
1720
  snap Take immediate multi-viewport screenshots of a URL & check console errors
1721
+ live Interactive control: click coordinates/selectors, type text, snap --full
1686
1722
  init Generate starter template in scenarios/template.scenario.ts and mocks.ts
1687
1723
 
1688
1724
  Options:
@@ -1690,6 +1726,7 @@ Options:
1690
1726
  --start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
1691
1727
  --start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
1692
1728
  --clean-artifacts Purge previous test artifacts to prevent folder bloat
1729
+ --full Capture full scrollable page instead of only the viewport
1693
1730
  --selector=<css> Target a specific element to focus on / resize-to-fit
1694
1731
  --viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
1695
1732
  --wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
@@ -1701,6 +1738,7 @@ Options:
1701
1738
  --port=<port> CDP remote debugging port [default: 9222]
1702
1739
  --build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
1703
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)
1704
1742
  --headed Show Chromium browser window
1705
1743
  --detach Keep browser/app open after finishing
1706
1744
  --dir=<path> Custom scenarios directory [default: scenarios]
@@ -1708,15 +1746,24 @@ Options:
1708
1746
  --folder=<path> Directory to save visual artifacts/reports (also --outDir)
1709
1747
  --help, -h Show this help message
1710
1748
  `);
1711
- }
1712
- function initScenarioTemplate() {
1713
- const targetDir = path8.resolve(process.cwd(), "scenarios");
1714
- if (!fs8.existsSync(targetDir)) {
1715
- fs8.mkdirSync(targetDir, { recursive: true });
1716
- }
1717
- const templatePath = path8.join(targetDir, "template.scenario.ts");
1718
- if (!fs8.existsSync(templatePath)) {
1719
- 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';
1720
1767
 
1721
1768
  export default defineVisualTest({
1722
1769
  id: 'template-check',
@@ -1746,14 +1793,14 @@ export default defineVisualTest({
1746
1793
  }
1747
1794
  });
1748
1795
  `;
1749
- fs8.writeFileSync(templatePath, templateContent, "utf8");
1750
- console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
1751
- } else {
1752
- console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
1753
- }
1754
- const mocksPath = path8.join(targetDir, "mocks.ts");
1755
- if (!fs8.existsSync(mocksPath)) {
1756
- 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 = `/**
1757
1804
  * Global IPC & API Mocks
1758
1805
  *
1759
1806
  * Export an array of base mocks to satisfy root application state on boot.
@@ -1763,161 +1810,408 @@ export default [
1763
1810
  { action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
1764
1811
  ];
1765
1812
  `;
1766
- fs8.writeFileSync(mocksPath, mocksContent, "utf8");
1767
- 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);
1768
1838
  }
1769
- console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
1770
1839
  }
1771
- if (options.help) {
1772
- printHelp();
1773
- 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;
1774
1858
  }
1775
- async function findScenarios(dir, specificName) {
1776
- const results = [];
1777
- if (!fs8.existsSync(dir)) return results;
1778
- const items = fs8.readdirSync(dir, { withFileTypes: true });
1779
- for (const item of items) {
1780
- const fullPath = path8.join(dir, item.name);
1781
- if (item.isDirectory()) {
1782
- results.push(...await findScenarios(fullPath, specificName));
1783
- } else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
1784
- if (!specificName || item.name.startsWith(specificName)) {
1785
- results.push(fullPath);
1786
- }
1787
- }
1788
- }
1789
- 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 {
1790
1897
  }
1791
- function resolveScenariosDirectory(customDir) {
1792
- if (customDir) {
1793
- return path8.resolve(process.cwd(), customDir);
1794
- }
1795
- const candidates = [
1796
- "scenarios",
1797
- "tests/visual",
1798
- "tests/scenarios",
1799
- "test/scenarios",
1800
- "src/scenarios"
1801
- ];
1802
- for (const c of candidates) {
1803
- const p = path8.resolve(process.cwd(), c);
1804
- if (fs8.existsSync(p)) {
1805
- return p;
1806
- }
1807
- }
1808
- 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;
1809
1996
  }
1810
- async function main() {
1811
- if (isSnapCommand || options.url && !options.scenario && !options.all) {
1812
- const snapSuccess = await runQuickSnap({
1813
- url: options.url,
1814
- start: options.start,
1815
- startCwd: options.startCwd,
1816
- selector: options.selector,
1817
- viewports: options.viewports,
1818
- waitMs: options.waitMs,
1819
- name: options.name,
1820
- clean: options.clean,
1821
- cleanArtifacts: options.cleanArtifacts,
1822
- headed: options.headed,
1823
- detach: options.detach,
1824
- outDir: options.outDir,
1825
- mode: options.mode,
1826
- exe: options.exe,
1827
- port: options.port
1828
- });
1829
- 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;
1830
2012
  }
1831
- if (!options.scenario && !options.all) {
1832
- console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
1833
- printHelp();
1834
- 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;
1835
2024
  }
1836
- const scenariosDir = resolveScenariosDirectory(options.dir);
1837
- const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
1838
- if (scenarioPaths.length === 0) {
1839
- console.error(`\u274C No scenarios found in ${scenariosDir}`);
1840
- 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);
1841
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);
1842
2089
  }
1843
- const shouldBuild = options.build || fileConfig.autoBuild;
1844
- if (shouldBuild) {
1845
- const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
1846
- console.log(`
1847
- \u{1F528} [Build] Running build process: "${buildCmd}"...`);
1848
- const { execSync } = __require("child_process");
1849
- try {
1850
- execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
1851
- } catch (e) {
1852
- console.error(`\u274C Build failed: ${buildCmd}`);
1853
- process.exit(1);
1854
- }
1855
- } else if (options.mode === "preview" && !options.url && !options.start) {
1856
- console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
1857
- }
1858
- console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
1859
- const jiti = createJiti(process.cwd());
1860
- let globalMocks = [];
1861
- const mockCandidates = [
1862
- path8.join(scenariosDir, "mocks.ts"),
1863
- path8.join(scenariosDir, "mocks.js"),
1864
- path8.join(process.cwd(), "mocks.ts"),
1865
- path8.join(process.cwd(), "mocks.js")
1866
- ];
1867
- for (const mocksPath of mockCandidates) {
1868
- if (fs8.existsSync(mocksPath)) {
1869
- try {
1870
- const m = await jiti.import(mocksPath);
1871
- globalMocks = m.default || m.mocks || [];
1872
- console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${path8.basename(mocksPath)}`);
1873
- break;
1874
- } catch (err) {
1875
- console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
1876
- }
1877
- }
2090
+ if (options.help) {
2091
+ printHelp();
2092
+ process.exit(0);
1878
2093
  }
1879
- let allSuccess = true;
1880
- for (const scenarioPath of scenarioPaths) {
1881
- try {
1882
- const scenarioModule = await jiti.import(scenarioPath);
1883
- const scenario = scenarioModule.default || scenarioModule.scenario;
1884
- if (!scenario || typeof scenario.run !== "function") {
1885
- console.error(`\u26A0\uFE0F Skipped: file ${path8.basename(scenarioPath)} does not export a VisualScenario object by default.`);
1886
- continue;
1887
- }
1888
- const result = await runVisualScenario({
1889
- scenario,
1890
- targetMode: options.mode,
2094
+ async function main() {
2095
+ if (isSnapCommand || options.url && !options.scenario && !options.all) {
2096
+ const snapSuccess = await runQuickSnap({
1891
2097
  url: options.url,
1892
- startCommand: options.start,
2098
+ start: options.start,
1893
2099
  startCwd: options.startCwd,
2100
+ selector: options.selector,
2101
+ viewports: options.viewports,
2102
+ waitMs: options.waitMs,
2103
+ name: options.name,
2104
+ clean: options.clean,
1894
2105
  cleanArtifacts: options.cleanArtifacts,
1895
- port: options.port,
1896
2106
  headed: options.headed,
1897
2107
  detach: options.detach,
1898
- executablePath: options.exe,
1899
- cleanPaths: options.clean,
1900
- desktopEnv: fileConfig.env,
1901
- wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
1902
- artifactsRoot: options.outDir ? path8.resolve(process.cwd(), options.outDir) : void 0,
1903
- 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
1904
2114
  });
1905
- 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);
1906
2203
  allSuccess = false;
1907
2204
  }
1908
- } catch (err) {
1909
- console.error(`\u274C Failed to load scenario ${path8.basename(scenarioPath)}:`, err);
1910
- allSuccess = false;
2205
+ }
2206
+ if (!allSuccess) {
2207
+ process.exit(1);
1911
2208
  }
1912
2209
  }
1913
- if (!allSuccess) {
2210
+ main().catch((err) => {
2211
+ console.error("Fatal error:", err);
1914
2212
  process.exit(1);
1915
- }
2213
+ });
1916
2214
  }
1917
- main().catch((err) => {
1918
- console.error("Fatal error:", err);
1919
- process.exit(1);
1920
- });
1921
2215
  }
1922
2216
  });
1923
2217
  export default require_cli();