@_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.
- package/README.md +141 -31
- package/dist/cli.d.mts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +1169 -957
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1268 -974
- package/dist/cli.mjs.map +1 -1
- package/dist/dsl-BIjVN1M0.d.mts +204 -0
- package/dist/dsl-BIjVN1M0.d.ts +204 -0
- package/dist/index.d.mts +131 -155
- package/dist/index.d.ts +131 -155
- package/dist/index.js +1506 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1479 -3
- package/dist/index.mjs.map +1 -1
- package/dist/plugins/a11y-tree/index.d.mts +11 -0
- package/dist/plugins/a11y-tree/index.d.ts +11 -0
- package/dist/plugins/a11y-tree/index.js +190 -0
- package/dist/plugins/a11y-tree/index.js.map +1 -0
- package/dist/plugins/a11y-tree/index.mjs +155 -0
- package/dist/plugins/a11y-tree/index.mjs.map +1 -0
- package/dist/plugins/desktop-webview2/index.d.mts +14 -0
- package/dist/plugins/desktop-webview2/index.d.ts +14 -0
- package/dist/plugins/desktop-webview2/index.js +258 -0
- package/dist/plugins/desktop-webview2/index.js.map +1 -0
- package/dist/plugins/desktop-webview2/index.mjs +221 -0
- package/dist/plugins/desktop-webview2/index.mjs.map +1 -0
- package/dist/plugins/live-controller/index.d.mts +35 -0
- package/dist/plugins/live-controller/index.d.ts +35 -0
- package/dist/plugins/live-controller/index.js +303 -0
- package/dist/plugins/live-controller/index.js.map +1 -0
- package/dist/plugins/live-controller/index.mjs +261 -0
- package/dist/plugins/live-controller/index.mjs.map +1 -0
- package/dist/plugins/mock-ipc/index.d.mts +30 -0
- package/dist/plugins/mock-ipc/index.d.ts +30 -0
- package/dist/plugins/mock-ipc/index.js +210 -0
- package/dist/plugins/mock-ipc/index.js.map +1 -0
- package/dist/plugins/mock-ipc/index.mjs +181 -0
- package/dist/plugins/mock-ipc/index.mjs.map +1 -0
- package/dist/plugins/visual-diff/index.d.mts +30 -0
- package/dist/plugins/visual-diff/index.d.ts +30 -0
- package/dist/plugins/visual-diff/index.js +163 -0
- package/dist/plugins/visual-diff/index.js.map +1 -0
- package/dist/plugins/visual-diff/index.mjs +127 -0
- package/dist/plugins/visual-diff/index.mjs.map +1 -0
- package/docs/plugins.md +415 -0
- package/package.json +40 -2
- package/skills/agent-lens/SKILL.md +142 -37
- package/skills/agent-lens/examples/06-state-testing-with-mock-ipc.md +13 -12
- package/skills/agent-lens/examples/07-live-controller-interactive-loop.md +108 -0
- package/skills/agent-lens/examples/08-accessibility-semantic-inspection.md +80 -0
- package/skills/agent-lens/examples/09-visual-regression-and-pixel-diffing.md +66 -0
- package/skills/agent-lens/examples/10-authoring-custom-agent-plugins.md +85 -0
- 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
|
|
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
|
|
129
|
+
/** Minimum supported window size */
|
|
26
130
|
MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
|
|
27
|
-
/** Standard default window size
|
|
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
|
|
39
|
-
import
|
|
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 (!
|
|
51
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
141
|
-
import
|
|
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 =
|
|
158
|
-
const reportPath =
|
|
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
|
-
|
|
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
|
|
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}](${
|
|
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
|
|
203
|
-
burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${
|
|
306
|
+
const relativeLink = frame.relativeUri || `./${frame.fileName}`;
|
|
307
|
+
burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
853
|
-
safePath =
|
|
613
|
+
if (!fs4.existsSync(safePath) || fs4.statSync(safePath).isDirectory()) {
|
|
614
|
+
safePath = path4.join(this.options.wwwrootDir, "index.html");
|
|
854
615
|
}
|
|
855
|
-
const ext =
|
|
616
|
+
const ext = path4.extname(safePath).toLowerCase();
|
|
856
617
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
857
618
|
try {
|
|
858
|
-
const content =
|
|
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
|
|
660
|
+
async addRouteMock(entry) {
|
|
899
661
|
if (!this.page) {
|
|
900
|
-
|
|
662
|
+
this.initialRouteMocks.push(entry);
|
|
663
|
+
return;
|
|
901
664
|
}
|
|
902
|
-
this.
|
|
903
|
-
|
|
904
|
-
(
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
|
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
|
-
|
|
951
|
-
|
|
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.
|
|
722
|
+
this.childProcess = spawn(command, {
|
|
967
723
|
cwd,
|
|
968
|
-
env,
|
|
969
|
-
shell:
|
|
724
|
+
env: { ...process.env, ...options?.env },
|
|
725
|
+
shell: true,
|
|
970
726
|
stdio: ["ignore", "pipe", "pipe"]
|
|
971
727
|
});
|
|
972
|
-
this.
|
|
973
|
-
const
|
|
974
|
-
|
|
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.
|
|
981
|
-
|
|
982
|
-
|
|
733
|
+
this.childProcess.stderr?.on("data", (chunk) => {
|
|
734
|
+
const line = chunk.toString().trim();
|
|
735
|
+
if (line && !line.includes("ExperimentalWarning")) {
|
|
736
|
+
}
|
|
983
737
|
});
|
|
984
|
-
this.
|
|
985
|
-
|
|
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.
|
|
748
|
+
if (this.childProcess && this.childProcess.exitCode !== null) {
|
|
996
749
|
throw new Error(
|
|
997
|
-
`[ProcessManager]
|
|
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, {
|
|
754
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
1006
755
|
if (response.status) {
|
|
1007
|
-
console.log(`\u2705 [ProcessManager]
|
|
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((
|
|
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 (
|
|
1027
|
-
this.child = null;
|
|
766
|
+
if (this.isStopped || !this.childProcess || !this.childProcess.pid) {
|
|
1028
767
|
return;
|
|
1029
768
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
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, "
|
|
773
|
+
treeKill(pid, "SIGTERM", (err) => {
|
|
1034
774
|
if (err) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
} catch {
|
|
1039
|
-
}
|
|
775
|
+
try {
|
|
776
|
+
treeKill(pid, "SIGKILL");
|
|
777
|
+
} catch {
|
|
1040
778
|
}
|
|
1041
779
|
}
|
|
1042
780
|
resolve();
|
|
1043
781
|
});
|
|
1044
782
|
});
|
|
1045
|
-
this.
|
|
783
|
+
this.childProcess = null;
|
|
1046
784
|
}
|
|
1047
785
|
};
|
|
1048
786
|
}
|
|
1049
787
|
});
|
|
1050
788
|
|
|
1051
|
-
// src/
|
|
1052
|
-
import
|
|
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 ||
|
|
1060
|
-
if (options.cleanArtifacts &&
|
|
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 =
|
|
1216
|
+
const entries = fs7.readdirSync(artifactsRoot, { withFileTypes: true });
|
|
1063
1217
|
for (const entry of entries) {
|
|
1064
|
-
|
|
1218
|
+
fs7.rmSync(path7.join(artifactsRoot, entry.name), { recursive: true, force: true });
|
|
1065
1219
|
}
|
|
1066
1220
|
}
|
|
1067
|
-
const scenarioArtifactsDir =
|
|
1068
|
-
if (!
|
|
1069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1375
|
-
|
|
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 =
|
|
1407
|
-
if (
|
|
1405
|
+
const resolvedCleanPath = path7.resolve(process.cwd(), cleanTarget);
|
|
1406
|
+
if (fs7.existsSync(resolvedCleanPath)) {
|
|
1408
1407
|
console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
|
|
1409
|
-
|
|
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 (
|
|
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
|
|
1440
|
-
import
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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 (
|
|
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
|
|
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 ?
|
|
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/
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1909
|
-
|
|
1910
|
-
|
|
2205
|
+
}
|
|
2206
|
+
if (!allSuccess) {
|
|
2207
|
+
process.exit(1);
|
|
1911
2208
|
}
|
|
1912
2209
|
}
|
|
1913
|
-
|
|
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();
|