@_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/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,19 +17,44 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
- // src/shared/api/dsl.ts
21
- var dsl_exports = {};
22
- __export(dsl_exports, {
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ CaptureEngine: () => CaptureEngine,
34
+ ConsoleTracker: () => ConsoleTracker,
35
+ PluginManager: () => PluginManager,
23
36
  VIEWPORT_PRESETS: () => VIEWPORT_PRESETS,
24
- defineVisualTest: () => defineVisualTest
37
+ VisualReporter: () => VisualReporter,
38
+ definePlugin: () => definePlugin,
39
+ defineVisualTest: () => defineVisualTest,
40
+ detectStartCwd: () => detectStartCwd,
41
+ loadConfig: () => loadConfig,
42
+ resolveWwwrootDir: () => resolveWwwrootDir,
43
+ runQuickSnap: () => runQuickSnap,
44
+ runVisualScenario: () => runVisualScenario
25
45
  });
26
- module.exports = __toCommonJS(dsl_exports);
46
+ module.exports = __toCommonJS(src_exports);
47
+
48
+ // src/shared/api/plugin.ts
49
+ function definePlugin(plugin) {
50
+ return plugin;
51
+ }
52
+
53
+ // src/shared/api/dsl.ts
27
54
  var VIEWPORT_PRESETS = {
28
- /** Minimum supported window size (e.g., ) */
55
+ /** Minimum supported window size */
29
56
  MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
30
- /** Standard default window size (e.g., ) */
57
+ /** Standard default window size */
31
58
  DEFAULT: { name: "default", width: 1200, height: 800 },
32
59
  /** Wide screen for checking grids and tables */
33
60
  WIDE: { name: "wide", width: 1600, height: 900 },
@@ -37,9 +64,1480 @@ var VIEWPORT_PRESETS = {
37
64
  function defineVisualTest(scenario) {
38
65
  return scenario;
39
66
  }
67
+
68
+ // src/features/runner/runner.ts
69
+ var import_path7 = __toESM(require("path"));
70
+ var import_fs7 = __toESM(require("fs"));
71
+
72
+ // src/features/capture/capture.ts
73
+ var import_fs = __toESM(require("fs"));
74
+ var import_path = __toESM(require("path"));
75
+ var CaptureEngine = class {
76
+ outputDir;
77
+ currentStepIndex = 0;
78
+ recordedSnapshots = [];
79
+ constructor(outputDir) {
80
+ this.outputDir = outputDir;
81
+ if (!import_fs.default.existsSync(this.outputDir)) {
82
+ import_fs.default.mkdirSync(this.outputDir, { recursive: true });
83
+ }
84
+ }
85
+ getSnapshots() {
86
+ return this.recordedSnapshots;
87
+ }
88
+ getOutputDir() {
89
+ return this.outputDir;
90
+ }
91
+ /**
92
+ */
93
+ async takeSnapshot(page, name, viewport, options) {
94
+ this.currentStepIndex += 1;
95
+ const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
96
+ const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
97
+ const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
98
+ const filePath = import_path.default.join(this.outputDir, fileName);
99
+ if (options?.selector) {
100
+ const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
101
+ await element.screenshot({ path: filePath });
102
+ } else {
103
+ await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
104
+ }
105
+ const metadata = {
106
+ index: this.currentStepIndex,
107
+ name,
108
+ fileName,
109
+ filePath,
110
+ relativeUri: `./${fileName}`,
111
+ viewport,
112
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
113
+ selector: options?.selector
114
+ };
115
+ this.recordedSnapshots.push(metadata);
116
+ return metadata;
117
+ }
118
+ /**
119
+ */
120
+ async takeBurst(page, name, viewport, options) {
121
+ this.currentStepIndex += 1;
122
+ const stepIndex = this.currentStepIndex;
123
+ const paddedIndex = String(stepIndex).padStart(2, "0");
124
+ const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
125
+ const duration = Math.max(options.durationMs, 50);
126
+ const interval = Math.max(options.intervalMs ?? 80, 20);
127
+ const totalFrames = Math.ceil(duration / interval);
128
+ const burstSnapshots = [];
129
+ const burstGroup = sanitizedName;
130
+ for (let frame = 0; frame <= totalFrames; frame++) {
131
+ const elapsedMs = frame * interval;
132
+ const paddedFrame = String(frame + 1).padStart(2, "0");
133
+ const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
134
+ const filePath = import_path.default.join(this.outputDir, fileName);
135
+ if (options.selector) {
136
+ const element = await page.$(options.selector);
137
+ if (element) {
138
+ await element.screenshot({ path: filePath });
139
+ } else {
140
+ await page.screenshot({ path: filePath });
141
+ }
142
+ } else {
143
+ await page.screenshot({ path: filePath });
144
+ }
145
+ const meta = {
146
+ index: stepIndex,
147
+ name: `${name} (frame ${frame + 1}, +${elapsedMs}ms)`,
148
+ fileName,
149
+ filePath,
150
+ relativeUri: `./${fileName}`,
151
+ viewport,
152
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
153
+ isBurstFrame: true,
154
+ burstGroup,
155
+ frameIndex: frame + 1,
156
+ selector: options.selector
157
+ };
158
+ burstSnapshots.push(meta);
159
+ this.recordedSnapshots.push(meta);
160
+ if (frame < totalFrames) {
161
+ await new Promise((r) => setTimeout(r, interval));
162
+ }
163
+ }
164
+ return burstSnapshots;
165
+ }
166
+ };
167
+
168
+ // src/features/reporter/reporter.ts
169
+ var import_fs2 = __toESM(require("fs"));
170
+ var import_path2 = __toESM(require("path"));
171
+ var VisualReporter = class {
172
+ static generateReport(data) {
173
+ const {
174
+ scenario,
175
+ snapshots,
176
+ consoleErrors,
177
+ consoleWarnings,
178
+ outputDir,
179
+ targetMode,
180
+ durationMs
181
+ } = data;
182
+ const manifestPath = import_path2.default.join(outputDir, "manifest.json");
183
+ const reportPath = import_path2.default.join(outputDir, "report.md");
184
+ const manifest = {
185
+ scenarioId: scenario.id,
186
+ title: scenario.title,
187
+ description: scenario.description,
188
+ targetMode,
189
+ durationMs,
190
+ totalSnapshots: snapshots.length,
191
+ consoleErrors: consoleErrors.length,
192
+ consoleWarnings: consoleWarnings.length,
193
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
194
+ snapshots,
195
+ errors: consoleErrors,
196
+ warnings: consoleWarnings
197
+ };
198
+ import_fs2.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
199
+ const rows = [];
200
+ const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
201
+ const burstGroups = /* @__PURE__ */ new Map();
202
+ snapshots.filter((s) => s.isBurstFrame && s.burstGroup).forEach((s) => {
203
+ const list = burstGroups.get(s.burstGroup) || [];
204
+ list.push(s);
205
+ burstGroups.set(s.burstGroup, list);
206
+ });
207
+ for (const snap of regularSnapshots) {
208
+ const relativeLink = snap.relativeUri || `./${snap.fileName}`;
209
+ rows.push(
210
+ `| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${relativeLink}) |`
211
+ );
212
+ }
213
+ let burstSections = "";
214
+ if (burstGroups.size > 0) {
215
+ burstSections += `
216
+ ### \u{1F3AC} Animation Bursts
217
+
218
+ `;
219
+ for (const [group, frames] of burstGroups.entries()) {
220
+ burstSections += `#### Animation: \`${group}\` (${frames.length} frames)
221
+
222
+ `;
223
+ burstSections += `| Frame | Viewport | File | Preview |
224
+ | :--- | :--- | :--- | :--- |
225
+ `;
226
+ for (const frame of frames) {
227
+ const relativeLink = frame.relativeUri || `./${frame.fileName}`;
228
+ burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${relativeLink}) | ![](${relativeLink}) |
229
+ `;
230
+ }
231
+ burstSections += `
232
+ `;
233
+ }
234
+ }
235
+ const healthStatus = this.getHealthStatus(consoleErrors, consoleWarnings);
236
+ let consoleSections = "";
237
+ if (consoleErrors.length > 0) {
238
+ consoleSections += `
239
+ ## \u{1F534} Console Errors (${consoleErrors.length})
240
+
241
+ `;
242
+ consoleSections += `> [!CAUTION]
243
+ > Found **${consoleErrors.length}** JavaScript errors during the test. This might indicate broken components, missing data, or unhandled exceptions.
244
+
245
+ `;
246
+ consoleSections += `| # | Time | Error | URL |
247
+ | :-: | :--- | :--- | :--- |
248
+ `;
249
+ consoleErrors.forEach((err, i) => {
250
+ const time = err.timestamp.split("T")[1]?.slice(0, 8) || "";
251
+ const text = err.text.replace(/\|/g, "\\|").slice(0, 200);
252
+ const url = err.url.replace(/\|/g, "\\|");
253
+ consoleSections += `| ${i + 1} | \`${time}\` | ${text} | \`${url}\` |
254
+ `;
255
+ });
256
+ const withStack = consoleErrors.filter((e) => e.stack);
257
+ if (withStack.length > 0) {
258
+ consoleSections += `
259
+ <details>
260
+ <summary>\u{1F4CB} Stack Traces (${withStack.length})</summary>
261
+
262
+ `;
263
+ withStack.forEach((err, i) => {
264
+ consoleSections += `**Error ${i + 1}:** \`${err.text.slice(0, 100)}\`
265
+ \`\`\`
266
+ ${err.stack}
267
+ \`\`\`
268
+
269
+ `;
270
+ });
271
+ consoleSections += `</details>
272
+ `;
273
+ }
274
+ }
275
+ if (consoleWarnings.length > 0) {
276
+ consoleSections += `
277
+ ## \u{1F7E1} Console Warnings (${consoleWarnings.length})
278
+
279
+ `;
280
+ consoleSections += `| # | Warning |
281
+ | :-: | :--- |
282
+ `;
283
+ consoleWarnings.slice(0, 20).forEach((warn, i) => {
284
+ const text = warn.text.replace(/\|/g, "\\|").slice(0, 200);
285
+ consoleSections += `| ${i + 1} | ${text} |
286
+ `;
287
+ });
288
+ if (consoleWarnings.length > 20) {
289
+ consoleSections += `
290
+ *...and ${consoleWarnings.length - 20} more warnings (full list in manifest.json)*
291
+ `;
292
+ }
293
+ }
294
+ let pluginSections = "";
295
+ if (data.customSections && data.customSections.length > 0) {
296
+ for (const sec of data.customSections) {
297
+ pluginSections += `
298
+ ## ${sec.title}
299
+
300
+ ${sec.content}
301
+
302
+ `;
303
+ }
304
+ }
305
+ const reportContent = `# \u{1F4F8} Visual Test Report: ${scenario.title}
306
+
307
+ > **Scenario ID:** \`${scenario.id}\`
308
+ > **Target Mode:** \`${targetMode.toUpperCase()}\` (${targetMode === "desktop" ? "Native Desktop App / WebView2" : "Fast Web Preview"})
309
+ > **Execution Duration:** ${(durationMs / 1e3).toFixed(2)} s
310
+ > **Total Snapshots Captured:** ${snapshots.length}
311
+ > **Health Status:** ${healthStatus}
312
+ > **Artifacts Folder:** \`${outputDir}\`
313
+
314
+ ---
315
+
316
+ ## \u{1F5BC}\uFE0F Primary Snapshot Overview
317
+
318
+ | # | Viewport | Step Description | File Reference |
319
+ | :-: | :--- | :--- | :--- |
320
+ ${rows.join("\n")}
321
+
322
+ ${burstSections}
323
+ ${consoleSections}
324
+ ${pluginSections}
325
+ ---
326
+
327
+ ## \u{1F4CB} AI Agent Verification Checklist:
328
+ - [ ] **Console Errors**: ${consoleErrors.length === 0 ? "\u2705 No errors found" : `\u274C ${consoleErrors.length} errors \u2014 MUST REVIEW`}
329
+ - [ ] **Visual Layout Check**: Inspect snapshots (e.g. view_image on \`./01_${regularSnapshots[0]?.fileName || "quick_snap"}\`) for layout shifts or clipped elements.
330
+ - [ ] **Responsiveness at \`1024x768\`**: Elements do not overflow the screen, no unwanted horizontal scroll.
331
+
332
+ - [ ] **Typography & Spacing**: Spacing matches the design system and layout grids.
333
+ - [ ] **Color Palette & Theme**: Background tints and button accent colors match the concept.
334
+ - [ ] **Component States**: Modals open centered, dropdowns do not overlap with other layers (z-index).
335
+ - [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
336
+
337
+ `;
338
+ import_fs2.default.writeFileSync(reportPath, reportContent, "utf-8");
339
+ return reportPath;
340
+ }
341
+ static getHealthStatus(errors, warnings) {
342
+ if (errors.length === 0 && warnings.length === 0) {
343
+ return "\u2705 Healthy \u2014 no errors or warnings";
344
+ }
345
+ if (errors.length === 0 && warnings.length > 0) {
346
+ return `\u26A0\uFE0F Warnings (${warnings.length}) \u2014 no critical errors`;
347
+ }
348
+ return `\u274C ERRORS (${errors.length} errors, ${warnings.length} warnings) \u2014 requires attention`;
349
+ }
350
+ };
351
+
352
+ // src/features/console-tracker/consoleTracker.ts
353
+ var ConsoleTracker = class {
354
+ entries = [];
355
+ attached = false;
356
+ attach(page) {
357
+ if (this.attached) return;
358
+ this.attached = true;
359
+ page.on("console", (msg) => {
360
+ const type = msg.type();
361
+ const level = this.mapConsoleType(type);
362
+ const entry = {
363
+ level,
364
+ text: msg.text(),
365
+ url: page.url(),
366
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
367
+ };
368
+ this.entries.push(entry);
369
+ if (level === "error") {
370
+ console.log(`\u{1F534} [Console ERROR] ${msg.text()}`);
371
+ } else if (level === "warning") {
372
+ if (!this.isIgnoredWarning(msg.text())) {
373
+ console.log(`\u{1F7E1} [Console WARN] ${msg.text()}`);
374
+ }
375
+ }
376
+ });
377
+ page.on("pageerror", (error) => {
378
+ const entry = {
379
+ level: "error",
380
+ text: error.message,
381
+ url: page.url(),
382
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
383
+ stack: error.stack
384
+ };
385
+ this.entries.push(entry);
386
+ console.log(`\u{1F4A5} [Page ERROR] ${error.message}`);
387
+ });
388
+ }
389
+ getErrors() {
390
+ return this.entries.filter((e) => e.level === "error");
391
+ }
392
+ getWarnings() {
393
+ return this.entries.filter((e) => e.level === "warning").filter((e) => !this.isIgnoredWarning(e.text));
394
+ }
395
+ getAll() {
396
+ return [...this.entries];
397
+ }
398
+ get errorCount() {
399
+ return this.getErrors().length;
400
+ }
401
+ get warningCount() {
402
+ return this.getWarnings().length;
403
+ }
404
+ get hasErrors() {
405
+ return this.errorCount > 0;
406
+ }
407
+ clear() {
408
+ this.entries = [];
409
+ }
410
+ mapConsoleType(type) {
411
+ switch (type) {
412
+ case "error":
413
+ return "error";
414
+ case "warning":
415
+ return "warning";
416
+ case "info":
417
+ return "info";
418
+ case "debug":
419
+ return "debug";
420
+ default:
421
+ return "log";
422
+ }
423
+ }
424
+ isIgnoredWarning(text) {
425
+ const ignoredPatterns = [
426
+ "findDOMNode is deprecated",
427
+ // Chromium DevTools
428
+ "DevTools",
429
+ // Playwright injection
430
+ "__playwright",
431
+ "[vite]",
432
+ // React 18 hydration warnings
433
+ "Extra attributes from the server",
434
+ "Download the React DevTools"
435
+ ];
436
+ return ignoredPatterns.some((pattern) => text.includes(pattern));
437
+ }
438
+ };
439
+
440
+ // src/shared/drivers/previewDriver.ts
441
+ var import_http = __toESM(require("http"));
442
+ var import_fs4 = __toESM(require("fs"));
443
+ var import_path4 = __toESM(require("path"));
444
+
445
+ // src/shared/lib/playwrightLoader.ts
446
+ var import_playwright = require("playwright");
447
+ var chromium = new Proxy(import_playwright.chromium, {
448
+ get(target, prop, receiver) {
449
+ if (prop === "launch") {
450
+ return async (...args) => {
451
+ try {
452
+ return await target.launch(...args);
453
+ } catch (err) {
454
+ if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
455
+ console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
456
+ console.error("\u{1F449} Please install it by running: npx playwright install chromium\n");
457
+ }
458
+ throw err;
459
+ }
460
+ };
461
+ }
462
+ const val = Reflect.get(target, prop, receiver);
463
+ return typeof val === "function" ? val.bind(target) : val;
464
+ }
465
+ });
466
+
467
+ // src/shared/lib/config.ts
468
+ var import_path3 = __toESM(require("path"));
469
+ var import_fs3 = __toESM(require("fs"));
470
+ function loadConfig(cwd = process.cwd()) {
471
+ const jsonConfigPath = import_path3.default.join(cwd, "agent-lens.json");
472
+ if (import_fs3.default.existsSync(jsonConfigPath)) {
473
+ try {
474
+ const raw = import_fs3.default.readFileSync(jsonConfigPath, "utf-8");
475
+ return JSON.parse(raw);
476
+ } catch (e) {
477
+ console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
478
+ }
479
+ }
480
+ const packageJsonPath = import_path3.default.join(cwd, "package.json");
481
+ if (import_fs3.default.existsSync(packageJsonPath)) {
482
+ try {
483
+ const raw = import_fs3.default.readFileSync(packageJsonPath, "utf-8");
484
+ const pkg = JSON.parse(raw);
485
+ if (pkg.agentLens && typeof pkg.agentLens === "object") {
486
+ return pkg.agentLens;
487
+ }
488
+ } catch {
489
+ }
490
+ }
491
+ return {};
492
+ }
493
+ function detectStartCwd(providedCwd) {
494
+ if (providedCwd) {
495
+ const resolved = import_path3.default.resolve(process.cwd(), providedCwd);
496
+ if (import_fs3.default.existsSync(resolved)) {
497
+ return providedCwd;
498
+ }
499
+ }
500
+ const rootPkgPath = import_path3.default.join(process.cwd(), "package.json");
501
+ let rootHasDevScript = false;
502
+ if (import_fs3.default.existsSync(rootPkgPath)) {
503
+ try {
504
+ const rootPkg = JSON.parse(import_fs3.default.readFileSync(rootPkgPath, "utf-8"));
505
+ rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
506
+ } catch {
507
+ }
508
+ }
509
+ if (rootHasDevScript) {
510
+ return void 0;
511
+ }
512
+ const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
513
+ for (const candidate of candidates) {
514
+ const candidatePkg = import_path3.default.join(process.cwd(), candidate, "package.json");
515
+ if (import_fs3.default.existsSync(candidatePkg)) {
516
+ return `./${candidate}`;
517
+ }
518
+ }
519
+ return void 0;
520
+ }
521
+ function resolveWwwrootDir(customDir) {
522
+ if (customDir) {
523
+ return import_path3.default.resolve(process.cwd(), customDir);
524
+ }
525
+ const candidates = [
526
+ "dist",
527
+ "build",
528
+ "out",
529
+ "wwwroot",
530
+ "Frontend/dist",
531
+ "frontend/dist",
532
+ "client/dist"
533
+ ];
534
+ for (const c of candidates) {
535
+ const candidatePath = import_path3.default.resolve(process.cwd(), c);
536
+ if (import_fs3.default.existsSync(candidatePath) && import_fs3.default.existsSync(import_path3.default.join(candidatePath, "index.html"))) {
537
+ return candidatePath;
538
+ }
539
+ }
540
+ for (const c of candidates) {
541
+ const candidatePath = import_path3.default.resolve(process.cwd(), c);
542
+ if (import_fs3.default.existsSync(candidatePath)) {
543
+ return candidatePath;
544
+ }
545
+ }
546
+ return import_path3.default.resolve(process.cwd(), "dist");
547
+ }
548
+
549
+ // src/shared/drivers/previewDriver.ts
550
+ var MIME_TYPES = {
551
+ ".html": "text/html; charset=utf-8",
552
+ ".js": "application/javascript; charset=utf-8",
553
+ ".css": "text/css; charset=utf-8",
554
+ ".json": "application/json; charset=utf-8",
555
+ ".png": "image/png",
556
+ ".jpg": "image/jpeg",
557
+ ".jpeg": "image/jpeg",
558
+ ".gif": "image/gif",
559
+ ".svg": "image/svg+xml",
560
+ ".ico": "image/x-icon",
561
+ ".woff": "font/woff",
562
+ ".woff2": "font/woff2",
563
+ ".ttf": "font/ttf"
564
+ };
565
+ var PreviewDriver = class {
566
+ server = null;
567
+ browser = null;
568
+ context = null;
569
+ page = null;
570
+ serverPort = 0;
571
+ options;
572
+ _baseUrl = "";
573
+ initialRouteMocks = [];
574
+ constructor(options) {
575
+ this.options = options || {};
576
+ if (!this.options.url) {
577
+ this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
578
+ }
579
+ }
580
+ get baseUrl() {
581
+ return this._baseUrl;
582
+ }
583
+ async start(initialViewport = { width: 1200, height: 800 }) {
584
+ let targetUrl = this.options.url;
585
+ if (!targetUrl) {
586
+ if (!import_fs4.default.existsSync(this.options.wwwrootDir)) {
587
+ throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
588
+ }
589
+ await new Promise((resolve, reject) => {
590
+ this.server = import_http.default.createServer((req, res) => {
591
+ let reqUrl = req.url?.split("?")[0] || "/";
592
+ if (reqUrl === "/") reqUrl = "/index.html";
593
+ let safePath = import_path4.default.normalize(import_path4.default.join(this.options.wwwrootDir, reqUrl));
594
+ if (!safePath.startsWith(this.options.wwwrootDir)) {
595
+ res.writeHead(403);
596
+ return res.end("Forbidden");
597
+ }
598
+ if (!import_fs4.default.existsSync(safePath) || import_fs4.default.statSync(safePath).isDirectory()) {
599
+ safePath = import_path4.default.join(this.options.wwwrootDir, "index.html");
600
+ }
601
+ const ext = import_path4.default.extname(safePath).toLowerCase();
602
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
603
+ try {
604
+ const content = import_fs4.default.readFileSync(safePath);
605
+ res.writeHead(200, { "Content-Type": contentType });
606
+ res.end(content);
607
+ } catch (e) {
608
+ res.writeHead(500);
609
+ res.end(`Server error: ${e.message}`);
610
+ }
611
+ });
612
+ this.server.listen(0, "127.0.0.1", () => {
613
+ const addr = this.server?.address();
614
+ if (typeof addr === "object" && addr?.port) {
615
+ this.serverPort = addr.port;
616
+ this._baseUrl = `http://127.0.0.1:${this.serverPort}`;
617
+ resolve();
618
+ } else {
619
+ reject(new Error("Failed to acquire port for static preview server"));
620
+ }
621
+ });
622
+ });
623
+ targetUrl = `${this._baseUrl}/index.html`;
624
+ } else {
625
+ this._baseUrl = targetUrl;
626
+ console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
627
+ }
628
+ this.browser = await chromium.launch({
629
+ headless: !this.options.headed,
630
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
631
+ });
632
+ this.context = await this.browser.newContext({
633
+ viewport: initialViewport,
634
+ deviceScaleFactor: 1
635
+ });
636
+ this.page = await this.context.newPage();
637
+ if (this.initialRouteMocks.length > 0) {
638
+ for (const entry of this.initialRouteMocks) {
639
+ await this.addRouteMock(entry);
640
+ }
641
+ }
642
+ await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
643
+ return { page: this.page, context: this.context, browser: this.browser };
644
+ }
645
+ async addRouteMock(entry) {
646
+ if (!this.page) {
647
+ this.initialRouteMocks.push(entry);
648
+ return;
649
+ }
650
+ await this.page.route(entry.url, async (route) => {
651
+ const req = route.request();
652
+ if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
653
+ return route.continue();
654
+ }
655
+ if (entry.delayMs) {
656
+ await new Promise((r) => setTimeout(r, entry.delayMs));
657
+ }
658
+ const isJson = typeof entry.body === "object" && entry.body !== null;
659
+ await route.fulfill({
660
+ status: entry.status ?? 200,
661
+ contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
662
+ body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
663
+ headers: entry.headers
664
+ });
665
+ });
666
+ }
667
+ async setupRouteMocks(routes) {
668
+ for (const r of routes) {
669
+ await this.addRouteMock(r);
670
+ }
671
+ }
672
+ async stop() {
673
+ if (this.context) {
674
+ await this.context.close().catch(() => {
675
+ });
676
+ this.context = null;
677
+ }
678
+ if (this.browser) {
679
+ await this.browser.close().catch(() => {
680
+ });
681
+ this.browser = null;
682
+ }
683
+ if (this.server) {
684
+ await new Promise((resolve) => {
685
+ this.server?.close(() => resolve());
686
+ });
687
+ this.server = null;
688
+ }
689
+ }
690
+ };
691
+
692
+ // src/shared/lib/processManager.ts
693
+ var import_child_process = require("child_process");
694
+ var import_tree_kill = __toESM(require("tree-kill"));
695
+ var ProcessManager = class {
696
+ childProcess = null;
697
+ isStopped = false;
698
+ async start(command, options) {
699
+ const cwd = options?.cwd || process.cwd();
700
+ console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
701
+ this.childProcess = (0, import_child_process.spawn)(command, {
702
+ cwd,
703
+ env: { ...process.env, ...options?.env },
704
+ shell: true,
705
+ stdio: ["ignore", "pipe", "pipe"]
706
+ });
707
+ this.childProcess.stdout?.on("data", (chunk) => {
708
+ const line = chunk.toString().trim();
709
+ if (line) {
710
+ }
711
+ });
712
+ this.childProcess.stderr?.on("data", (chunk) => {
713
+ const line = chunk.toString().trim();
714
+ if (line && !line.includes("ExperimentalWarning")) {
715
+ }
716
+ });
717
+ this.childProcess.on("exit", (code, signal) => {
718
+ if (!this.isStopped && code !== 0 && code !== null) {
719
+ console.warn(`\u26A0\uFE0F [ProcessManager] Subprocess exited prematurely with code ${code}, signal ${signal}`);
720
+ }
721
+ });
722
+ }
723
+ async waitForUrl(url, timeoutMs = 3e4) {
724
+ console.log(`\u23F3 [ProcessManager] Waiting for ${url} to respond...`);
725
+ const startTime = Date.now();
726
+ while (Date.now() - startTime < timeoutMs) {
727
+ if (this.childProcess && this.childProcess.exitCode !== null) {
728
+ throw new Error(
729
+ `[ProcessManager] Server process exited with code ${this.childProcess.exitCode} while waiting for ${url}`
730
+ );
731
+ }
732
+ try {
733
+ const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
734
+ if (response.status) {
735
+ console.log(`\u2705 [ProcessManager] Target ${url} is ready (status: ${response.status})!`);
736
+ return true;
737
+ }
738
+ } catch {
739
+ }
740
+ await new Promise((r) => setTimeout(r, 500));
741
+ }
742
+ throw new Error(`[ProcessManager] Timeout after ${timeoutMs}ms waiting for ${url} to respond.`);
743
+ }
744
+ async stop() {
745
+ if (this.isStopped || !this.childProcess || !this.childProcess.pid) {
746
+ return;
747
+ }
748
+ this.isStopped = true;
749
+ const pid = this.childProcess.pid;
750
+ console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
751
+ await new Promise((resolve) => {
752
+ (0, import_tree_kill.default)(pid, "SIGTERM", (err) => {
753
+ if (err) {
754
+ try {
755
+ (0, import_tree_kill.default)(pid, "SIGKILL");
756
+ } catch {
757
+ }
758
+ }
759
+ resolve();
760
+ });
761
+ });
762
+ this.childProcess = null;
763
+ }
764
+ };
765
+
766
+ // src/shared/lib/pluginLoader.ts
767
+ var import_path5 = __toESM(require("path"));
768
+ var import_fs5 = __toESM(require("fs"));
769
+ var import_jiti = require("jiti");
770
+ function findPackageRoot() {
771
+ let cur = __dirname;
772
+ while (cur !== import_path5.default.dirname(cur)) {
773
+ const pkgPath = import_path5.default.join(cur, "package.json");
774
+ if (import_fs5.default.existsSync(pkgPath)) {
775
+ try {
776
+ const pkg = JSON.parse(import_fs5.default.readFileSync(pkgPath, "utf-8"));
777
+ if (pkg.name === "@_deep4wee/agent-lens" || pkg.name === "agent-lens") {
778
+ return cur;
779
+ }
780
+ } catch {
781
+ }
782
+ }
783
+ cur = import_path5.default.dirname(cur);
784
+ }
785
+ return import_path5.default.resolve(__dirname, "..");
786
+ }
787
+ var PluginManager = class {
788
+ plugins = [];
789
+ jiti = (0, import_jiti.createJiti)(process.cwd());
790
+ register(plugin) {
791
+ if (this.plugins.some((p) => p.name === plugin.name)) {
792
+ return;
793
+ }
794
+ this.plugins.push(plugin);
795
+ console.log(`\u{1F50C} [Plugin] Registered: ${plugin.name}${plugin.version ? ` (v${plugin.version})` : ""}`);
796
+ }
797
+ async load(pluginSpec) {
798
+ if (typeof pluginSpec === "object" && pluginSpec !== null) {
799
+ this.register(pluginSpec);
800
+ return;
801
+ }
802
+ const pluginNameOrPath = pluginSpec.trim();
803
+ if (!pluginNameOrPath) return;
804
+ if (pluginNameOrPath.startsWith(".") || pluginNameOrPath.startsWith("/") || pluginNameOrPath.includes("/") || pluginNameOrPath.includes("\\")) {
805
+ const resolvedPath = import_path5.default.resolve(process.cwd(), pluginNameOrPath);
806
+ await this.loadFromFile(resolvedPath);
807
+ return;
808
+ }
809
+ const localCandidates = [
810
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.ts`),
811
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.js`),
812
+ import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.ts`),
813
+ import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.js`),
814
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.ts"),
815
+ import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.js"),
816
+ import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.ts"),
817
+ import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.js")
818
+ ];
819
+ for (const candidate of localCandidates) {
820
+ if (import_fs5.default.existsSync(candidate)) {
821
+ await this.loadFromFile(candidate);
822
+ return;
823
+ }
824
+ }
825
+ const pkgRoot = findPackageRoot();
826
+ const builtInCandidates = [
827
+ import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.ts"),
828
+ import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.js"),
829
+ import_path5.default.join(pkgRoot, "dist", "plugins", pluginNameOrPath, "index.js"),
830
+ import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.ts"),
831
+ import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.js"),
832
+ import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.ts"),
833
+ import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.js"),
834
+ import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.ts"),
835
+ import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.js")
836
+ ];
837
+ for (const builtIn of builtInCandidates) {
838
+ if (import_fs5.default.existsSync(builtIn)) {
839
+ await this.loadFromFile(builtIn);
840
+ return;
841
+ }
842
+ }
843
+ try {
844
+ const mod = await this.jiti.import(pluginNameOrPath);
845
+ const plugin = mod.default || mod.plugin || mod;
846
+ if (plugin && plugin.name) {
847
+ this.register(plugin);
848
+ } else {
849
+ console.warn(`\u26A0\uFE0F [Plugin] Package "${pluginNameOrPath}" did not export a valid AgentLensPlugin.`);
850
+ }
851
+ } catch (err) {
852
+ console.warn(`\u26A0\uFE0F [Plugin] Could not load plugin '${pluginNameOrPath}': ${err.message}`);
853
+ }
854
+ }
855
+ async loadFromFile(filePath) {
856
+ if (!import_fs5.default.existsSync(filePath)) {
857
+ console.warn(`\u26A0\uFE0F [Plugin] Plugin file not found: ${filePath}`);
858
+ return;
859
+ }
860
+ try {
861
+ const mod = await this.jiti.import(filePath);
862
+ const plugin = mod.default || mod.plugin || mod;
863
+ if (plugin && plugin.name) {
864
+ this.register(plugin);
865
+ } else {
866
+ console.warn(`\u26A0\uFE0F [Plugin] File "${filePath}" does not export a valid AgentLensPlugin by default.`);
867
+ }
868
+ } catch (err) {
869
+ console.error(`\u274C [Plugin] Failed to import plugin from ${filePath}:`, err.message);
870
+ }
871
+ }
872
+ async loadAll(specs) {
873
+ for (const spec of specs) {
874
+ await this.load(spec);
875
+ }
876
+ }
877
+ async runSetup(hookContext) {
878
+ for (const p of this.plugins) {
879
+ if (p.setup) await p.setup(hookContext);
880
+ }
881
+ }
882
+ async launchSession(options, hookContext) {
883
+ for (const p of this.plugins) {
884
+ if (p.launchSession) {
885
+ const session = await p.launchSession(options, hookContext);
886
+ if (session) {
887
+ return session;
888
+ }
889
+ }
890
+ }
891
+ return void 0;
892
+ }
893
+ async runOnContextCreated(context, hookContext) {
894
+ for (const p of this.plugins) {
895
+ if (p.onContextCreated) await p.onContextCreated(context, hookContext);
896
+ }
897
+ }
898
+ async runOnPageCreated(page, context, hookContext) {
899
+ for (const p of this.plugins) {
900
+ if (p.onPageCreated) await p.onPageCreated(page, context, hookContext);
901
+ }
902
+ }
903
+ async extendContext(ctx, page, hookContext) {
904
+ for (const p of this.plugins) {
905
+ if (p.extendContext) {
906
+ const extensions = await p.extendContext(ctx, page, hookContext);
907
+ if (extensions && typeof extensions === "object") {
908
+ Object.assign(ctx, extensions);
909
+ }
910
+ }
911
+ }
912
+ }
913
+ async runOnAfterRun(reportData, hookContext) {
914
+ for (const p of this.plugins) {
915
+ if (p.onAfterRun) await p.onAfterRun(reportData, hookContext);
916
+ }
917
+ }
918
+ async runTeardown(hookContext) {
919
+ for (const p of this.plugins) {
920
+ if (p.teardown) {
921
+ try {
922
+ await p.teardown(hookContext);
923
+ } catch (e) {
924
+ console.error(`\u26A0\uFE0F [Plugin] Error in ${p.name}.teardown:`, e.message);
925
+ }
926
+ }
927
+ }
928
+ }
929
+ get loadedPlugins() {
930
+ return [...this.plugins];
931
+ }
932
+ };
933
+
934
+ // src/features/runner/lib/navigation.ts
935
+ function createNavigator(page, baseUrl, targetUrl) {
936
+ return async (route) => {
937
+ if (route.startsWith("http://") || route.startsWith("https://")) {
938
+ await page.goto(route, { waitUntil: "domcontentloaded" });
939
+ await page.waitForTimeout(300);
940
+ return;
941
+ }
942
+ if (route.startsWith("#")) {
943
+ await page.evaluate((r) => {
944
+ window.location.hash = r;
945
+ }, route);
946
+ await page.waitForTimeout(300);
947
+ return;
948
+ }
949
+ if (baseUrl && targetUrl) {
950
+ try {
951
+ const fullUrl = new URL(route, baseUrl).toString();
952
+ await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
953
+ await page.waitForTimeout(300);
954
+ return;
955
+ } catch {
956
+ }
957
+ }
958
+ const routePath = route.startsWith("/") ? route : `/${route}`;
959
+ await page.evaluate((r) => {
960
+ if (window.location.hash !== void 0) {
961
+ window.location.hash = r;
962
+ }
963
+ }, routePath);
964
+ await page.waitForTimeout(300);
965
+ };
966
+ }
967
+
968
+ // src/features/runner/lib/artifactsSync.ts
969
+ var import_fs6 = __toESM(require("fs"));
970
+ var import_path6 = __toESM(require("path"));
971
+ function syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots) {
972
+ try {
973
+ const latestDir = import_path6.default.join(artifactsRoot, "latest");
974
+ if (import_fs6.default.existsSync(latestDir)) {
975
+ import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
976
+ }
977
+ import_fs6.default.mkdirSync(latestDir, { recursive: true });
978
+ if (import_fs6.default.existsSync(reportPath)) {
979
+ import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
980
+ }
981
+ const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
982
+ if (import_fs6.default.existsSync(manifestSrc)) {
983
+ import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
984
+ }
985
+ for (const snap of snapshots) {
986
+ if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
987
+ import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
988
+ }
989
+ }
990
+ return import_path6.default.join(latestDir, "report.md");
991
+ } catch {
992
+ return void 0;
993
+ }
994
+ }
995
+
996
+ // src/features/runner/lib/contextBuilder.ts
997
+ function buildTestContext(options) {
998
+ const {
999
+ page,
1000
+ context,
1001
+ targetMode,
1002
+ initialViewport,
1003
+ captureEngine,
1004
+ consoleTracker,
1005
+ previewDriver,
1006
+ doNavigate
1007
+ } = options;
1008
+ let currentViewport = { ...initialViewport };
1009
+ const ctx = {
1010
+ page,
1011
+ context,
1012
+ targetMode,
1013
+ currentViewport,
1014
+ // --- Snapshots ---
1015
+ capture: async (name, opts) => {
1016
+ console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
1017
+ return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
1018
+ },
1019
+ captureBurst: async (name, opts) => {
1020
+ console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
1021
+ return await captureEngine.takeBurst(page, name, currentViewport, opts);
1022
+ },
1023
+ // --- Navigation ---
1024
+ navigate: async (route) => {
1025
+ console.log(`\u{1F9ED} [Navigate] ${route}`);
1026
+ await doNavigate(route);
1027
+ },
1028
+ // --- Viewport Management ---
1029
+ resize: async (width, height) => {
1030
+ console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
1031
+ currentViewport = { width, height };
1032
+ ctx.currentViewport = currentViewport;
1033
+ await page.setViewportSize(currentViewport);
1034
+ await page.waitForTimeout(200);
1035
+ },
1036
+ setPreset: async (preset) => {
1037
+ console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
1038
+ await ctx.resize(preset.width, preset.height);
1039
+ },
1040
+ resizeToFit: async (selector, padding = 0) => {
1041
+ let boundingBox;
1042
+ if (selector) {
1043
+ const el = await page.$(selector);
1044
+ if (el) {
1045
+ boundingBox = await el.boundingBox();
1046
+ }
1047
+ } else {
1048
+ boundingBox = await page.evaluate(() => ({
1049
+ width: document.documentElement.scrollWidth,
1050
+ height: document.documentElement.scrollHeight
1051
+ }));
1052
+ }
1053
+ if (boundingBox) {
1054
+ const newWidth = Math.ceil(boundingBox.width) + padding * 2;
1055
+ const newHeight = Math.ceil(boundingBox.height) + padding * 2;
1056
+ console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
1057
+ await ctx.resize(newWidth, newHeight);
1058
+ } else {
1059
+ console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
1060
+ }
1061
+ },
1062
+ // --- Waiting & DOM Observation ---
1063
+ wait: async (ms) => {
1064
+ await page.waitForTimeout(ms);
1065
+ },
1066
+ waitForSelector: async (selector, timeoutMs = 5e3) => {
1067
+ await page.waitForSelector(selector, { timeout: timeoutMs });
1068
+ },
1069
+ // --- Interaction ---
1070
+ click: async (selector) => {
1071
+ console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
1072
+ await page.click(selector);
1073
+ },
1074
+ rightClick: async (selector) => {
1075
+ console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
1076
+ await page.click(selector, { button: "right" });
1077
+ },
1078
+ type: async (selector, text) => {
1079
+ console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
1080
+ await page.fill(selector, text);
1081
+ },
1082
+ selectOption: async (selector, value) => {
1083
+ console.log(`\u2705 [Select] ${selector} -> "${value}"`);
1084
+ await page.selectOption(selector, value);
1085
+ },
1086
+ hover: async (selector) => {
1087
+ console.log(`\u{1F446} [Hover] ${selector}`);
1088
+ await page.hover(selector);
1089
+ },
1090
+ scroll: async (selector, deltaY) => {
1091
+ console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
1092
+ await page.evaluate(
1093
+ ({ sel, dY }) => {
1094
+ const el = document.querySelector(sel);
1095
+ if (el) {
1096
+ el.scrollTop += dY;
1097
+ } else {
1098
+ window.scrollBy(0, dY);
1099
+ }
1100
+ },
1101
+ { sel: selector, dY: deltaY }
1102
+ );
1103
+ await page.waitForTimeout(100);
1104
+ },
1105
+ // --- Logging ---
1106
+ log: (msg) => {
1107
+ console.log(`\u2139\uFE0F [Scenario] ${msg}`);
1108
+ },
1109
+ // --- Mock IPC (delegated or stubbed if plugin not loaded) ---
1110
+ setMockIpc: async (action, data, mockOptions) => {
1111
+ console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc called for "${action}". Ensure --plugin=mock-ipc is enabled.`);
1112
+ },
1113
+ // --- HTTP Route Mocking ---
1114
+ setMockRoute: async (url, body, routeOptions) => {
1115
+ if (!previewDriver) {
1116
+ console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored (no preview driver running)`);
1117
+ return;
1118
+ }
1119
+ console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
1120
+ await previewDriver.addRouteMock({
1121
+ url,
1122
+ body,
1123
+ method: routeOptions?.method,
1124
+ status: routeOptions?.status,
1125
+ delayMs: routeOptions?.delayMs,
1126
+ headers: routeOptions?.headers
1127
+ });
1128
+ },
1129
+ // --- Console Errors ---
1130
+ getConsoleErrors: () => consoleTracker.getErrors(),
1131
+ getConsoleWarnings: () => consoleTracker.getWarnings(),
1132
+ hasConsoleErrors: () => consoleTracker.hasErrors,
1133
+ // --- DOM Assertions ---
1134
+ readText: async (selector) => {
1135
+ const text = await page.textContent(selector);
1136
+ return text ? text.trim() : null;
1137
+ },
1138
+ getPageText: async () => {
1139
+ return await page.evaluate(() => document.body.innerText || "");
1140
+ },
1141
+ isVisible: async (selector) => {
1142
+ try {
1143
+ const element = await page.$(selector);
1144
+ if (!element) return false;
1145
+ return await element.isVisible();
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ },
1150
+ getElementCount: async (selector) => {
1151
+ const elements = await page.$$(selector);
1152
+ return elements.length;
1153
+ }
1154
+ };
1155
+ return {
1156
+ ctx,
1157
+ getCurrentViewport: () => currentViewport
1158
+ };
1159
+ }
1160
+
1161
+ // src/features/runner/runner.ts
1162
+ async function runVisualScenario(options) {
1163
+ const { scenario } = options;
1164
+ const targetMode = options.targetMode || "preview";
1165
+ const startTime = Date.now();
1166
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1167
+ const artifactsRoot = options.artifactsRoot || import_path7.default.resolve(process.cwd(), "artifacts");
1168
+ if (options.cleanArtifacts && import_fs7.default.existsSync(artifactsRoot)) {
1169
+ console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
1170
+ const entries = import_fs7.default.readdirSync(artifactsRoot, { withFileTypes: true });
1171
+ for (const entry of entries) {
1172
+ import_fs7.default.rmSync(import_path7.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
1173
+ }
1174
+ }
1175
+ const scenarioArtifactsDir = import_path7.default.join(artifactsRoot, `${scenario.id}_${timestamp}`);
1176
+ if (!import_fs7.default.existsSync(scenarioArtifactsDir)) {
1177
+ import_fs7.default.mkdirSync(scenarioArtifactsDir, { recursive: true });
1178
+ }
1179
+ const captureEngine = new CaptureEngine(scenarioArtifactsDir);
1180
+ const consoleTracker = new ConsoleTracker();
1181
+ const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
1182
+ const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
1183
+ let previewDriver = null;
1184
+ let processManager = null;
1185
+ let customDriverStop = null;
1186
+ const pluginManager = new PluginManager();
1187
+ const pluginSpecs = [...options.plugins || [], ...scenario.plugins || []];
1188
+ if (pluginSpecs.length > 0) {
1189
+ await pluginManager.loadAll(pluginSpecs);
1190
+ }
1191
+ const hookContext = {
1192
+ scenario,
1193
+ targetMode,
1194
+ artifactsDir: scenarioArtifactsDir,
1195
+ cliOptions: options,
1196
+ state: /* @__PURE__ */ new Map()
1197
+ };
1198
+ try {
1199
+ let page;
1200
+ let context;
1201
+ console.log(`
1202
+ ========================================`);
1203
+ console.log(`\u{1F680} Starting Visual Test: ${scenario.title}`);
1204
+ console.log(`\u{1F3AF} Mode: ${targetMode.toUpperCase()}`);
1205
+ console.log(`\u{1F4C1} Artifacts: ${scenarioArtifactsDir}`);
1206
+ console.log(`========================================
1207
+ `);
1208
+ if (options.startCommand) {
1209
+ processManager = new ProcessManager();
1210
+ await processManager.start(options.startCommand, { cwd: options.startCwd });
1211
+ const waitTarget = options.url || "http://localhost:5173";
1212
+ await processManager.waitForUrl(waitTarget);
1213
+ }
1214
+ await pluginManager.runSetup(hookContext);
1215
+ if (typeof scenario.setup === "function") {
1216
+ console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
1217
+ await scenario.setup();
1218
+ }
1219
+ const customSession = await pluginManager.launchSession(
1220
+ { currentViewport, headed: options.headed },
1221
+ hookContext
1222
+ );
1223
+ if (customSession) {
1224
+ page = customSession.page;
1225
+ context = customSession.context;
1226
+ if (customSession.stop) {
1227
+ customDriverStop = customSession.stop;
1228
+ }
1229
+ } else if (targetMode === "desktop") {
1230
+ throw new Error(
1231
+ `[AgentLens] Target mode is 'desktop', but no desktop plugin is registered. Please add --plugin=desktop-webview2 to your command or configuration.`
1232
+ );
1233
+ } else {
1234
+ previewDriver = new PreviewDriver({
1235
+ wwwrootDir: options.wwwrootDir,
1236
+ url: options.url,
1237
+ headed: options.headed
1238
+ });
1239
+ if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1240
+ console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
1241
+ await previewDriver.setupRouteMocks(scenario.mockRoutes);
1242
+ }
1243
+ const res = await previewDriver.start(currentViewport);
1244
+ page = res.page;
1245
+ context = res.context;
1246
+ if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
1247
+ await previewDriver.setupRouteMocks(scenario.mockRoutes);
1248
+ }
1249
+ }
1250
+ await pluginManager.runOnContextCreated(context, hookContext);
1251
+ await pluginManager.runOnPageCreated(page, context, hookContext);
1252
+ consoleTracker.attach(page);
1253
+ console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
1254
+ `);
1255
+ const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
1256
+ if (scenario.route) {
1257
+ console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
1258
+ await doNavigate(scenario.route);
1259
+ }
1260
+ const { ctx } = buildTestContext({
1261
+ page,
1262
+ context,
1263
+ targetMode,
1264
+ initialViewport: currentViewport,
1265
+ captureEngine,
1266
+ consoleTracker,
1267
+ previewDriver,
1268
+ doNavigate
1269
+ });
1270
+ await pluginManager.extendContext(ctx, page, hookContext);
1271
+ await scenario.run(ctx);
1272
+ const durationMs = Date.now() - startTime;
1273
+ const snapshots = captureEngine.getSnapshots();
1274
+ const consoleErrors = consoleTracker.getErrors();
1275
+ const consoleWarnings = consoleTracker.getWarnings();
1276
+ if (consoleErrors.length > 0) {
1277
+ console.log(`
1278
+ \u{1F534} Console Errors: ${consoleErrors.length}`);
1279
+ consoleErrors.forEach((err, i) => console.log(` ${i + 1}. ${err.text.slice(0, 120)}`));
1280
+ }
1281
+ if (consoleWarnings.length > 0) {
1282
+ console.log(`
1283
+ \u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
1284
+ }
1285
+ const reportData = {
1286
+ scenario,
1287
+ snapshots,
1288
+ consoleErrors,
1289
+ consoleWarnings,
1290
+ outputDir: scenarioArtifactsDir,
1291
+ targetMode,
1292
+ durationMs,
1293
+ customSections: []
1294
+ };
1295
+ await pluginManager.runOnAfterRun(reportData, hookContext);
1296
+ const reportPath = VisualReporter.generateReport(reportData);
1297
+ const latestReport = syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1298
+ console.log(`
1299
+ \u2705 Visual Test Completed Successfully!`);
1300
+ console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
1301
+ console.log(`\u{1F534} Console Errors: ${consoleErrors.length}`);
1302
+ console.log(`\u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
1303
+ console.log(`\u{1F4C4} Markdown Report: ${reportPath}`);
1304
+ if (latestReport) {
1305
+ console.log(`\u{1F4CC} Latest Report: ${latestReport}`);
1306
+ }
1307
+ return {
1308
+ scenarioId: scenario.id,
1309
+ targetMode,
1310
+ success: true,
1311
+ totalSnapshots: snapshots.length,
1312
+ consoleErrors: consoleErrors.length,
1313
+ consoleWarnings: consoleWarnings.length,
1314
+ reportPath,
1315
+ artifactsDir: scenarioArtifactsDir
1316
+ };
1317
+ } catch (err) {
1318
+ console.error(`
1319
+ \u274C Visual Test Failed: ${err.message}`);
1320
+ const durationMs = Date.now() - startTime;
1321
+ const snapshots = captureEngine.getSnapshots();
1322
+ const consoleErrors = consoleTracker.getErrors();
1323
+ const consoleWarnings = consoleTracker.getWarnings();
1324
+ const reportData = {
1325
+ scenario,
1326
+ snapshots,
1327
+ consoleErrors,
1328
+ consoleWarnings,
1329
+ outputDir: scenarioArtifactsDir,
1330
+ targetMode,
1331
+ durationMs
1332
+ };
1333
+ const reportPath = VisualReporter.generateReport(reportData);
1334
+ syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
1335
+ return {
1336
+ scenarioId: scenario.id,
1337
+ targetMode,
1338
+ success: false,
1339
+ totalSnapshots: snapshots.length,
1340
+ consoleErrors: consoleErrors.length,
1341
+ consoleWarnings: consoleWarnings.length,
1342
+ reportPath,
1343
+ artifactsDir: scenarioArtifactsDir,
1344
+ error: err.message
1345
+ };
1346
+ } finally {
1347
+ await pluginManager.runTeardown(hookContext);
1348
+ if (typeof scenario.teardown === "function") {
1349
+ try {
1350
+ console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
1351
+ await scenario.teardown();
1352
+ } catch (teardownErr) {
1353
+ console.error(`\u26A0\uFE0F [Scenario Teardown] Error during teardown:`, teardownErr.message);
1354
+ }
1355
+ }
1356
+ if (options.cleanPaths && options.cleanPaths.length > 0) {
1357
+ for (const cleanTarget of options.cleanPaths) {
1358
+ try {
1359
+ const resolvedCleanPath = import_path7.default.resolve(process.cwd(), cleanTarget);
1360
+ if (import_fs7.default.existsSync(resolvedCleanPath)) {
1361
+ console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
1362
+ import_fs7.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
1363
+ }
1364
+ } catch (cleanErr) {
1365
+ console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
1366
+ }
1367
+ }
1368
+ }
1369
+ if (!options.detach) {
1370
+ if (customDriverStop) await customDriverStop();
1371
+ if (previewDriver) await previewDriver.stop();
1372
+ if (processManager) await processManager.stop();
1373
+ } else {
1374
+ console.log("\u{1F517} [Runner] Detach mode active, leaving browser/app open.");
1375
+ }
1376
+ }
1377
+ }
1378
+
1379
+ // src/features/snap/snap.ts
1380
+ var import_path8 = __toESM(require("path"));
1381
+ var import_fs8 = __toESM(require("fs"));
1382
+ var PRESET_MAP = {
1383
+ default: VIEWPORT_PRESETS.DEFAULT,
1384
+ desktop: VIEWPORT_PRESETS.DEFAULT,
1385
+ min: VIEWPORT_PRESETS.MIN_SUPPORTED,
1386
+ "min-supported": VIEWPORT_PRESETS.MIN_SUPPORTED,
1387
+ wide: VIEWPORT_PRESETS.WIDE,
1388
+ mobile: { name: "mobile", width: 375, height: 667 },
1389
+ tablet: { name: "tablet", width: 768, height: 1024 },
1390
+ "full-hd": VIEWPORT_PRESETS.FULL_HD
1391
+ };
1392
+ function parseViewportPresets(raw) {
1393
+ if (!raw || raw.length === 0) {
1394
+ return [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
1395
+ }
1396
+ const presets = [];
1397
+ for (const item of raw) {
1398
+ const lower = item.toLowerCase().trim();
1399
+ if (PRESET_MAP[lower]) {
1400
+ presets.push(PRESET_MAP[lower]);
1401
+ } else if (lower.includes("x")) {
1402
+ const [w, h] = lower.split("x").map((n) => parseInt(n, 10));
1403
+ if (!isNaN(w) && !isNaN(h)) {
1404
+ presets.push({ name: `${w}x${h}`, width: w, height: h });
1405
+ }
1406
+ }
1407
+ }
1408
+ return presets.length > 0 ? presets : [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
1409
+ }
1410
+ var CANDIDATE_PORTS = [5173, 3e3, 4321, 4200, 8080, 8e3, 3001];
1411
+ async function isPortResponding(url) {
1412
+ try {
1413
+ const res = await fetch(url, { signal: AbortSignal.timeout(800) });
1414
+ return Boolean(res.status);
1415
+ } catch {
1416
+ return false;
1417
+ }
1418
+ }
1419
+ async function detectActiveDevServer() {
1420
+ for (const port of CANDIDATE_PORTS) {
1421
+ const url = `http://localhost:${port}`;
1422
+ if (await isPortResponding(url)) {
1423
+ return url;
1424
+ }
1425
+ }
1426
+ return null;
1427
+ }
1428
+ async function runQuickSnap(options) {
1429
+ let targetUrl = options.url;
1430
+ let useStaticPreview = false;
1431
+ let wwwrootDir;
1432
+ if (!targetUrl && !options.start) {
1433
+ const activeUrl = await detectActiveDevServer();
1434
+ if (activeUrl) {
1435
+ targetUrl = activeUrl;
1436
+ console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
1437
+ } else {
1438
+ const candidateDir = resolveWwwrootDir();
1439
+ if (import_fs8.default.existsSync(candidateDir) && import_fs8.default.existsSync(import_path8.default.join(candidateDir, "index.html"))) {
1440
+ useStaticPreview = true;
1441
+ wwwrootDir = candidateDir;
1442
+ console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
1443
+ } else {
1444
+ console.error(`
1445
+ \u274C [Quick Snap] No active server found on common ports (${CANDIDATE_PORTS.join(", ")}), and no static build found.`);
1446
+ console.log(`\u{1F4A1} Suggested actions:`);
1447
+ console.log(` 1. Pass a start command: npx agent-lens snap --start="npm run dev"`);
1448
+ console.log(` 2. Specify your URL: npx agent-lens snap --url=http://localhost:8080`);
1449
+ console.log(` 3. Build your static app: npm run build
1450
+ `);
1451
+ return false;
1452
+ }
1453
+ }
1454
+ } else if (!targetUrl && options.start) {
1455
+ targetUrl = "http://localhost:5173";
1456
+ }
1457
+ const waitMs = options.waitMs ?? 1e3;
1458
+ const snapshotPrefix = options.name || "quick_snap";
1459
+ const targetViewports = parseViewportPresets(options.viewports);
1460
+ console.log(`
1461
+ \u{1F4F8} [Quick Snap] Preparing instant verification for: ${targetUrl || wwwrootDir}`);
1462
+ console.log(`\u{1F4D0} [Quick Snap] Testing ${targetViewports.length} viewports: ${targetViewports.map((v) => `${v.name} (${v.width}x${v.height})`).join(", ")}`);
1463
+ if (options.selector) {
1464
+ console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${options.selector}"`);
1465
+ }
1466
+ const snapScenario = defineVisualTest({
1467
+ id: "quick-snap",
1468
+ title: `Quick Verification: ${targetUrl || "Static Preview"}`,
1469
+ description: `One-shot automated snapshot and console health check`,
1470
+ route: targetUrl || "/",
1471
+ viewports: targetViewports,
1472
+ run: async (ctx) => {
1473
+ ctx.log(`Waiting ${waitMs}ms for page stabilization...`);
1474
+ await ctx.wait(waitMs);
1475
+ for (let i = 0; i < targetViewports.length; i++) {
1476
+ const vp = targetViewports[i];
1477
+ const stepNum = String(i + 1).padStart(2, "0");
1478
+ ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
1479
+ await ctx.setPreset(vp);
1480
+ await ctx.wait(200);
1481
+ await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
1482
+ }
1483
+ if (options.selector) {
1484
+ ctx.log(`Focusing on selector: "${options.selector}"`);
1485
+ try {
1486
+ await ctx.setPreset(VIEWPORT_PRESETS.DEFAULT);
1487
+ await ctx.resizeToFit(options.selector, 15);
1488
+ await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector: options.selector });
1489
+ } catch (err) {
1490
+ ctx.log(`\u26A0\uFE0F Could not isolate selector "${options.selector}": ${err.message}`);
1491
+ }
1492
+ }
1493
+ const errors = ctx.getConsoleErrors();
1494
+ if (errors.length > 0) {
1495
+ ctx.log(`\u{1F6A8} Caught ${errors.length} console errors during snap check!`);
1496
+ } else {
1497
+ ctx.log(`\u2705 Clean run: No console errors detected.`);
1498
+ }
1499
+ }
1500
+ });
1501
+ const result = await runVisualScenario({
1502
+ scenario: snapScenario,
1503
+ targetMode: options.mode || "preview",
1504
+ url: useStaticPreview ? void 0 : targetUrl,
1505
+ wwwrootDir: useStaticPreview ? wwwrootDir : void 0,
1506
+ startCommand: options.start,
1507
+ startCwd: options.startCwd,
1508
+ executablePath: options.exe,
1509
+ cleanPaths: options.clean,
1510
+ cleanArtifacts: options.cleanArtifacts,
1511
+ port: options.port,
1512
+ headed: options.headed,
1513
+ detach: options.detach,
1514
+ artifactsRoot: options.outDir ? import_path8.default.resolve(process.cwd(), options.outDir) : void 0,
1515
+ plugins: options.plugins
1516
+ });
1517
+ console.log(`
1518
+ ========================================`);
1519
+ console.log(`\u{1F3C1} Quick Snap Finished!`);
1520
+ console.log(`\u{1F4F8} Snapshots: ${result.totalSnapshots}`);
1521
+ console.log(`\u{1F534} Console Errors: ${result.consoleErrors}`);
1522
+ console.log(`\u{1F7E1} Console Warnings: ${result.consoleWarnings}`);
1523
+ console.log(`\u{1F4C4} Report: ${result.reportPath}`);
1524
+ console.log(`========================================
1525
+ `);
1526
+ return result.success && result.consoleErrors === 0;
1527
+ }
40
1528
  // Annotate the CommonJS export names for ESM import in node:
41
1529
  0 && (module.exports = {
1530
+ CaptureEngine,
1531
+ ConsoleTracker,
1532
+ PluginManager,
42
1533
  VIEWPORT_PRESETS,
43
- defineVisualTest
1534
+ VisualReporter,
1535
+ definePlugin,
1536
+ defineVisualTest,
1537
+ detectStartCwd,
1538
+ loadConfig,
1539
+ resolveWwwrootDir,
1540
+ runQuickSnap,
1541
+ runVisualScenario
44
1542
  });
45
1543
  //# sourceMappingURL=index.js.map