@_deep4wee/agent-lens 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -227
- package/dist/cli.js +1068 -911
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1277 -1033
- 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 -178
- package/dist/index.d.ts +131 -178
- 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 +130 -43
- 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.js
CHANGED
|
@@ -23,19 +23,102 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/app/cli.ts
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
26
|
+
var import_path13 = __toESM(require("path"));
|
|
27
|
+
var import_fs12 = __toESM(require("fs"));
|
|
28
|
+
var import_child_process2 = require("child_process");
|
|
29
|
+
var import_jiti2 = require("jiti");
|
|
30
|
+
|
|
31
|
+
// src/shared/lib/config.ts
|
|
32
|
+
var import_path = __toESM(require("path"));
|
|
33
|
+
var import_fs = __toESM(require("fs"));
|
|
34
|
+
function loadConfig(cwd = process.cwd()) {
|
|
35
|
+
const jsonConfigPath = import_path.default.join(cwd, "agent-lens.json");
|
|
36
|
+
if (import_fs.default.existsSync(jsonConfigPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = import_fs.default.readFileSync(jsonConfigPath, "utf-8");
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const packageJsonPath = import_path.default.join(cwd, "package.json");
|
|
45
|
+
if (import_fs.default.existsSync(packageJsonPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const raw = import_fs.default.readFileSync(packageJsonPath, "utf-8");
|
|
48
|
+
const pkg = JSON.parse(raw);
|
|
49
|
+
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
50
|
+
return pkg.agentLens;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
function detectStartCwd(providedCwd) {
|
|
58
|
+
if (providedCwd) {
|
|
59
|
+
const resolved = import_path.default.resolve(process.cwd(), providedCwd);
|
|
60
|
+
if (import_fs.default.existsSync(resolved)) {
|
|
61
|
+
return providedCwd;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const rootPkgPath = import_path.default.join(process.cwd(), "package.json");
|
|
65
|
+
let rootHasDevScript = false;
|
|
66
|
+
if (import_fs.default.existsSync(rootPkgPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const rootPkg = JSON.parse(import_fs.default.readFileSync(rootPkgPath, "utf-8"));
|
|
69
|
+
rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (rootHasDevScript) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
const candidatePkg = import_path.default.join(process.cwd(), candidate, "package.json");
|
|
79
|
+
if (import_fs.default.existsSync(candidatePkg)) {
|
|
80
|
+
return `./${candidate}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
function resolveWwwrootDir(customDir) {
|
|
86
|
+
if (customDir) {
|
|
87
|
+
return import_path.default.resolve(process.cwd(), customDir);
|
|
88
|
+
}
|
|
89
|
+
const candidates = [
|
|
90
|
+
"dist",
|
|
91
|
+
"build",
|
|
92
|
+
"out",
|
|
93
|
+
"wwwroot",
|
|
94
|
+
"Frontend/dist",
|
|
95
|
+
"frontend/dist",
|
|
96
|
+
"client/dist"
|
|
97
|
+
];
|
|
98
|
+
for (const c of candidates) {
|
|
99
|
+
const candidatePath = import_path.default.resolve(process.cwd(), c);
|
|
100
|
+
if (import_fs.default.existsSync(candidatePath) && import_fs.default.existsSync(import_path.default.join(candidatePath, "index.html"))) {
|
|
101
|
+
return candidatePath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const c of candidates) {
|
|
105
|
+
const candidatePath = import_path.default.resolve(process.cwd(), c);
|
|
106
|
+
if (import_fs.default.existsSync(candidatePath)) {
|
|
107
|
+
return candidatePath;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return import_path.default.resolve(process.cwd(), "dist");
|
|
111
|
+
}
|
|
29
112
|
|
|
30
113
|
// src/features/runner/runner.ts
|
|
31
|
-
var
|
|
32
|
-
var
|
|
114
|
+
var import_path7 = __toESM(require("path"));
|
|
115
|
+
var import_fs7 = __toESM(require("fs"));
|
|
33
116
|
|
|
34
117
|
// src/shared/api/dsl.ts
|
|
35
118
|
var VIEWPORT_PRESETS = {
|
|
36
|
-
/** Minimum supported window size
|
|
119
|
+
/** Minimum supported window size */
|
|
37
120
|
MIN_SUPPORTED: { name: "min-supported", width: 1024, height: 768 },
|
|
38
|
-
/** Standard default window size
|
|
121
|
+
/** Standard default window size */
|
|
39
122
|
DEFAULT: { name: "default", width: 1200, height: 800 },
|
|
40
123
|
/** Wide screen for checking grids and tables */
|
|
41
124
|
WIDE: { name: "wide", width: 1600, height: 900 },
|
|
@@ -47,16 +130,16 @@ function defineVisualTest(scenario) {
|
|
|
47
130
|
}
|
|
48
131
|
|
|
49
132
|
// src/features/capture/capture.ts
|
|
50
|
-
var
|
|
51
|
-
var
|
|
133
|
+
var import_fs2 = __toESM(require("fs"));
|
|
134
|
+
var import_path2 = __toESM(require("path"));
|
|
52
135
|
var CaptureEngine = class {
|
|
53
136
|
outputDir;
|
|
54
137
|
currentStepIndex = 0;
|
|
55
138
|
recordedSnapshots = [];
|
|
56
139
|
constructor(outputDir) {
|
|
57
140
|
this.outputDir = outputDir;
|
|
58
|
-
if (!
|
|
59
|
-
|
|
141
|
+
if (!import_fs2.default.existsSync(this.outputDir)) {
|
|
142
|
+
import_fs2.default.mkdirSync(this.outputDir, { recursive: true });
|
|
60
143
|
}
|
|
61
144
|
}
|
|
62
145
|
getSnapshots() {
|
|
@@ -67,17 +150,17 @@ var CaptureEngine = class {
|
|
|
67
150
|
}
|
|
68
151
|
/**
|
|
69
152
|
*/
|
|
70
|
-
async takeSnapshot(page, name, viewport,
|
|
153
|
+
async takeSnapshot(page, name, viewport, options) {
|
|
71
154
|
this.currentStepIndex += 1;
|
|
72
155
|
const paddedIndex = String(this.currentStepIndex).padStart(2, "0");
|
|
73
156
|
const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
74
157
|
const fileName = `${paddedIndex}_${sanitizedName}_${viewport.width}x${viewport.height}.png`;
|
|
75
|
-
const filePath =
|
|
76
|
-
if (
|
|
77
|
-
const element = await page.waitForSelector(
|
|
158
|
+
const filePath = import_path2.default.join(this.outputDir, fileName);
|
|
159
|
+
if (options?.selector) {
|
|
160
|
+
const element = await page.waitForSelector(options.selector, { timeout: 5e3 });
|
|
78
161
|
await element.screenshot({ path: filePath });
|
|
79
162
|
} else {
|
|
80
|
-
await page.screenshot({ path: filePath, fullPage:
|
|
163
|
+
await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
|
|
81
164
|
}
|
|
82
165
|
const metadata = {
|
|
83
166
|
index: this.currentStepIndex,
|
|
@@ -87,20 +170,20 @@ var CaptureEngine = class {
|
|
|
87
170
|
relativeUri: `./${fileName}`,
|
|
88
171
|
viewport,
|
|
89
172
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
-
selector:
|
|
173
|
+
selector: options?.selector
|
|
91
174
|
};
|
|
92
175
|
this.recordedSnapshots.push(metadata);
|
|
93
176
|
return metadata;
|
|
94
177
|
}
|
|
95
178
|
/**
|
|
96
179
|
*/
|
|
97
|
-
async takeBurst(page, name, viewport,
|
|
180
|
+
async takeBurst(page, name, viewport, options) {
|
|
98
181
|
this.currentStepIndex += 1;
|
|
99
182
|
const stepIndex = this.currentStepIndex;
|
|
100
183
|
const paddedIndex = String(stepIndex).padStart(2, "0");
|
|
101
184
|
const sanitizedName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
102
|
-
const duration = Math.max(
|
|
103
|
-
const interval = Math.max(
|
|
185
|
+
const duration = Math.max(options.durationMs, 50);
|
|
186
|
+
const interval = Math.max(options.intervalMs ?? 80, 20);
|
|
104
187
|
const totalFrames = Math.ceil(duration / interval);
|
|
105
188
|
const burstSnapshots = [];
|
|
106
189
|
const burstGroup = sanitizedName;
|
|
@@ -108,9 +191,9 @@ var CaptureEngine = class {
|
|
|
108
191
|
const elapsedMs = frame * interval;
|
|
109
192
|
const paddedFrame = String(frame + 1).padStart(2, "0");
|
|
110
193
|
const fileName = `${paddedIndex}_burst_${sanitizedName}_f${paddedFrame}_${elapsedMs}ms.png`;
|
|
111
|
-
const filePath =
|
|
112
|
-
if (
|
|
113
|
-
const element = await page.$(
|
|
194
|
+
const filePath = import_path2.default.join(this.outputDir, fileName);
|
|
195
|
+
if (options.selector) {
|
|
196
|
+
const element = await page.$(options.selector);
|
|
114
197
|
if (element) {
|
|
115
198
|
await element.screenshot({ path: filePath });
|
|
116
199
|
} else {
|
|
@@ -130,7 +213,7 @@ var CaptureEngine = class {
|
|
|
130
213
|
isBurstFrame: true,
|
|
131
214
|
burstGroup,
|
|
132
215
|
frameIndex: frame + 1,
|
|
133
|
-
selector:
|
|
216
|
+
selector: options.selector
|
|
134
217
|
};
|
|
135
218
|
burstSnapshots.push(meta);
|
|
136
219
|
this.recordedSnapshots.push(meta);
|
|
@@ -143,8 +226,8 @@ var CaptureEngine = class {
|
|
|
143
226
|
};
|
|
144
227
|
|
|
145
228
|
// src/features/reporter/reporter.ts
|
|
146
|
-
var
|
|
147
|
-
var
|
|
229
|
+
var import_fs3 = __toESM(require("fs"));
|
|
230
|
+
var import_path3 = __toESM(require("path"));
|
|
148
231
|
var VisualReporter = class {
|
|
149
232
|
static generateReport(data) {
|
|
150
233
|
const {
|
|
@@ -156,8 +239,8 @@ var VisualReporter = class {
|
|
|
156
239
|
targetMode,
|
|
157
240
|
durationMs
|
|
158
241
|
} = data;
|
|
159
|
-
const manifestPath =
|
|
160
|
-
const reportPath =
|
|
242
|
+
const manifestPath = import_path3.default.join(outputDir, "manifest.json");
|
|
243
|
+
const reportPath = import_path3.default.join(outputDir, "report.md");
|
|
161
244
|
const manifest = {
|
|
162
245
|
scenarioId: scenario.id,
|
|
163
246
|
title: scenario.title,
|
|
@@ -172,7 +255,7 @@ var VisualReporter = class {
|
|
|
172
255
|
errors: consoleErrors,
|
|
173
256
|
warnings: consoleWarnings
|
|
174
257
|
};
|
|
175
|
-
|
|
258
|
+
import_fs3.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
176
259
|
const rows = [];
|
|
177
260
|
const regularSnapshots = snapshots.filter((s) => !s.isBurstFrame);
|
|
178
261
|
const burstGroups = /* @__PURE__ */ new Map();
|
|
@@ -265,6 +348,17 @@ ${err.stack}
|
|
|
265
348
|
if (consoleWarnings.length > 20) {
|
|
266
349
|
consoleSections += `
|
|
267
350
|
*...and ${consoleWarnings.length - 20} more warnings (full list in manifest.json)*
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
let pluginSections = "";
|
|
355
|
+
if (data.customSections && data.customSections.length > 0) {
|
|
356
|
+
for (const sec of data.customSections) {
|
|
357
|
+
pluginSections += `
|
|
358
|
+
## ${sec.title}
|
|
359
|
+
|
|
360
|
+
${sec.content}
|
|
361
|
+
|
|
268
362
|
`;
|
|
269
363
|
}
|
|
270
364
|
}
|
|
@@ -287,6 +381,7 @@ ${rows.join("\n")}
|
|
|
287
381
|
|
|
288
382
|
${burstSections}
|
|
289
383
|
${consoleSections}
|
|
384
|
+
${pluginSections}
|
|
290
385
|
---
|
|
291
386
|
|
|
292
387
|
## \u{1F4CB} AI Agent Verification Checklist:
|
|
@@ -300,7 +395,7 @@ ${consoleSections}
|
|
|
300
395
|
- [ ] **Localization**: Verify that there are no raw i18n keys (text with dots like \`sidebar.home\` instead of "Home").
|
|
301
396
|
|
|
302
397
|
`;
|
|
303
|
-
|
|
398
|
+
import_fs3.default.writeFileSync(reportPath, reportContent, "utf-8");
|
|
304
399
|
return reportPath;
|
|
305
400
|
}
|
|
306
401
|
static getHealthStatus(errors, warnings) {
|
|
@@ -402,20 +497,19 @@ var ConsoleTracker = class {
|
|
|
402
497
|
}
|
|
403
498
|
};
|
|
404
499
|
|
|
405
|
-
// src/shared/drivers/
|
|
406
|
-
var import_child_process = require("child_process");
|
|
500
|
+
// src/shared/drivers/previewDriver.ts
|
|
407
501
|
var import_http = __toESM(require("http"));
|
|
408
|
-
var
|
|
409
|
-
var
|
|
502
|
+
var import_fs4 = __toESM(require("fs"));
|
|
503
|
+
var import_path4 = __toESM(require("path"));
|
|
410
504
|
|
|
411
505
|
// src/shared/lib/playwrightLoader.ts
|
|
412
506
|
var import_playwright = require("playwright");
|
|
413
507
|
var chromium = new Proxy(import_playwright.chromium, {
|
|
414
508
|
get(target, prop, receiver) {
|
|
415
509
|
if (prop === "launch") {
|
|
416
|
-
return async (...
|
|
510
|
+
return async (...args) => {
|
|
417
511
|
try {
|
|
418
|
-
return await target.launch(...
|
|
512
|
+
return await target.launch(...args);
|
|
419
513
|
} catch (err) {
|
|
420
514
|
if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
|
|
421
515
|
console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
|
|
@@ -430,357 +524,6 @@ var chromium = new Proxy(import_playwright.chromium, {
|
|
|
430
524
|
}
|
|
431
525
|
});
|
|
432
526
|
|
|
433
|
-
// src/shared/drivers/desktopDriver.ts
|
|
434
|
-
var import_tree_kill = __toESM(require("tree-kill"));
|
|
435
|
-
var DesktopDriver = class {
|
|
436
|
-
port;
|
|
437
|
-
executablePath;
|
|
438
|
-
autoLaunch;
|
|
439
|
-
args;
|
|
440
|
-
env;
|
|
441
|
-
cwd;
|
|
442
|
-
childProcess = null;
|
|
443
|
-
browser = null;
|
|
444
|
-
context = null;
|
|
445
|
-
page = null;
|
|
446
|
-
processStderr = "";
|
|
447
|
-
processExited = false;
|
|
448
|
-
exitCode = null;
|
|
449
|
-
constructor(options2) {
|
|
450
|
-
this.port = options2?.port || 9222;
|
|
451
|
-
this.autoLaunch = options2?.autoLaunch ?? true;
|
|
452
|
-
this.executablePath = options2?.executablePath;
|
|
453
|
-
this.args = options2?.args || [];
|
|
454
|
-
this.env = options2?.env || {};
|
|
455
|
-
this.cwd = options2?.cwd;
|
|
456
|
-
}
|
|
457
|
-
async isPortAvailable() {
|
|
458
|
-
return new Promise((resolve) => {
|
|
459
|
-
const req = import_http.default.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
|
|
460
|
-
resolve(res.statusCode === 200);
|
|
461
|
-
});
|
|
462
|
-
req.on("error", () => resolve(false));
|
|
463
|
-
req.setTimeout(800, () => {
|
|
464
|
-
req.destroy();
|
|
465
|
-
resolve(false);
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
async waitForPort(timeoutMs = 25e3) {
|
|
470
|
-
const startTime = Date.now();
|
|
471
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
472
|
-
if (this.processExited) {
|
|
473
|
-
throw new Error(
|
|
474
|
-
`[DesktopDriver] Process terminated prematurely with exit code ${this.exitCode}.
|
|
475
|
-
Stderr: ${this.processStderr.trim() || "(none)"}`
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
if (await this.isPortAvailable()) {
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
482
|
-
}
|
|
483
|
-
throw new Error(
|
|
484
|
-
`Timeout waiting for WebView2/Chromium CDP port ${this.port}. Last stderr:
|
|
485
|
-
${this.processStderr.trim() || "(no stderr output)"}`
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
async start(initialViewport = { width: 1200, height: 800 }) {
|
|
489
|
-
const alreadyRunning = await this.isPortAvailable();
|
|
490
|
-
if (!alreadyRunning) {
|
|
491
|
-
if (!this.autoLaunch) {
|
|
492
|
-
throw new Error(
|
|
493
|
-
`App is not running on port ${this.port} and autoLaunch is false. Start app with WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=${this.port}`
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
if (!this.executablePath) {
|
|
497
|
-
throw new Error(
|
|
498
|
-
`[DesktopDriver] No executablePath provided and nothing is running on port ${this.port}. Specify --exe=<path> in CLI or executablePath in config.`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
const resolvedExe = import_path3.default.resolve(process.cwd(), this.executablePath);
|
|
502
|
-
if (!import_fs3.default.existsSync(resolvedExe)) {
|
|
503
|
-
throw new Error(
|
|
504
|
-
`[DesktopDriver] Desktop executable not found at: ${resolvedExe}. Please build your native project first.`
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
console.log(`[DesktopDriver] Launching: ${resolvedExe}`);
|
|
508
|
-
const finalArgs = [...this.args];
|
|
509
|
-
const hasDebugPort = finalArgs.some((a) => a.startsWith("--remote-debugging-port="));
|
|
510
|
-
if (!hasDebugPort) {
|
|
511
|
-
finalArgs.push(`--remote-debugging-port=${this.port}`);
|
|
512
|
-
}
|
|
513
|
-
const mergedEnv = {
|
|
514
|
-
...process.env,
|
|
515
|
-
...this.env,
|
|
516
|
-
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${this.port}`
|
|
517
|
-
};
|
|
518
|
-
this.processStderr = "";
|
|
519
|
-
this.processExited = false;
|
|
520
|
-
this.exitCode = null;
|
|
521
|
-
this.childProcess = (0, import_child_process.spawn)(resolvedExe, finalArgs, {
|
|
522
|
-
env: mergedEnv,
|
|
523
|
-
cwd: this.cwd || import_path3.default.dirname(resolvedExe),
|
|
524
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
525
|
-
detached: false
|
|
526
|
-
});
|
|
527
|
-
;
|
|
528
|
-
this.processStderr = "";
|
|
529
|
-
this.processExited = false;
|
|
530
|
-
this.exitCode = null;
|
|
531
|
-
this.childProcess = (0, import_child_process.spawn)(resolvedExe, this.args, {
|
|
532
|
-
env: mergedEnv,
|
|
533
|
-
cwd: this.cwd || import_path3.default.dirname(resolvedExe),
|
|
534
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
535
|
-
detached: false
|
|
536
|
-
});
|
|
537
|
-
this.childProcess.stderr?.on("data", (chunk) => {
|
|
538
|
-
this.processStderr += chunk.toString();
|
|
539
|
-
});
|
|
540
|
-
this.childProcess.on("exit", (code) => {
|
|
541
|
-
this.processExited = true;
|
|
542
|
-
this.exitCode = code;
|
|
543
|
-
});
|
|
544
|
-
this.childProcess.on("error", (err) => {
|
|
545
|
-
console.error("[DesktopDriver] Failed to spawn process:", err);
|
|
546
|
-
});
|
|
547
|
-
console.log(`[DesktopDriver] Waiting for CDP debugging port on ${this.port}...`);
|
|
548
|
-
await this.waitForPort();
|
|
549
|
-
} else {
|
|
550
|
-
console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
|
|
551
|
-
}
|
|
552
|
-
console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
|
|
553
|
-
this.browser = await chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
|
|
554
|
-
const contexts = this.browser.contexts();
|
|
555
|
-
this.context = contexts[0] || await this.browser.newContext();
|
|
556
|
-
const pages = this.context.pages();
|
|
557
|
-
if (pages.length > 0) {
|
|
558
|
-
this.page = pages[0];
|
|
559
|
-
} else {
|
|
560
|
-
this.page = await this.context.waitForEvent("page", { timeout: 1e4 });
|
|
561
|
-
}
|
|
562
|
-
if (initialViewport) {
|
|
563
|
-
await this.page.setViewportSize(initialViewport).catch(() => {
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
return { page: this.page, context: this.context, browser: this.browser };
|
|
567
|
-
}
|
|
568
|
-
async stop() {
|
|
569
|
-
if (this.browser) {
|
|
570
|
-
await this.browser.close().catch(() => {
|
|
571
|
-
});
|
|
572
|
-
this.browser = null;
|
|
573
|
-
}
|
|
574
|
-
if (this.childProcess && !this.childProcess.killed && this.childProcess.pid) {
|
|
575
|
-
console.log("[DesktopDriver] Terminating spawned desktop process tree...");
|
|
576
|
-
const pid = this.childProcess.pid;
|
|
577
|
-
await new Promise((resolve) => {
|
|
578
|
-
(0, import_tree_kill.default)(pid, "SIGTERM", () => resolve());
|
|
579
|
-
});
|
|
580
|
-
this.childProcess = null;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
// src/shared/drivers/previewDriver.ts
|
|
586
|
-
var import_http2 = __toESM(require("http"));
|
|
587
|
-
var import_fs5 = __toESM(require("fs"));
|
|
588
|
-
var import_path5 = __toESM(require("path"));
|
|
589
|
-
|
|
590
|
-
// src/features/mock-ipc/mockIpc.ts
|
|
591
|
-
var MockIpcRegistry = class {
|
|
592
|
-
mocks = /* @__PURE__ */ new Map();
|
|
593
|
-
set(action, data, options2) {
|
|
594
|
-
this.mocks.set(action, {
|
|
595
|
-
action,
|
|
596
|
-
data,
|
|
597
|
-
type: options2?.type ?? "SUCCESS",
|
|
598
|
-
delayMs: options2?.delayMs ?? 20
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
setBatch(entries) {
|
|
602
|
-
for (const entry of entries) {
|
|
603
|
-
this.set(entry.action, entry.data, { type: entry.type, delayMs: entry.delayMs });
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
remove(action) {
|
|
607
|
-
this.mocks.delete(action);
|
|
608
|
-
}
|
|
609
|
-
clear() {
|
|
610
|
-
this.mocks.clear();
|
|
611
|
-
}
|
|
612
|
-
get(action) {
|
|
613
|
-
return this.mocks.get(action) ?? null;
|
|
614
|
-
}
|
|
615
|
-
toSerializable() {
|
|
616
|
-
const result = {};
|
|
617
|
-
for (const [action, entry] of this.mocks.entries()) {
|
|
618
|
-
result[action] = {
|
|
619
|
-
data: entry.data,
|
|
620
|
-
type: entry.type ?? "SUCCESS",
|
|
621
|
-
delayMs: entry.delayMs ?? 20
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
return result;
|
|
625
|
-
}
|
|
626
|
-
get size() {
|
|
627
|
-
return this.mocks.size;
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
function generateMockIpcScript(registry) {
|
|
631
|
-
const mocksJson = JSON.stringify(registry.toSerializable());
|
|
632
|
-
return `
|
|
633
|
-
(() => {
|
|
634
|
-
const __mockTable = ${mocksJson};
|
|
635
|
-
|
|
636
|
-
window.__visualRunnerMocks = __mockTable;
|
|
637
|
-
|
|
638
|
-
// Generic IPC mock bridge for modern web applications
|
|
639
|
-
window.__mockIpc = {
|
|
640
|
-
invoke: (action, payload) => {
|
|
641
|
-
return new Promise((resolve, reject) => {
|
|
642
|
-
const mock = window.__visualRunnerMocks[action];
|
|
643
|
-
|
|
644
|
-
if (mock) {
|
|
645
|
-
setTimeout(() => {
|
|
646
|
-
if (mock.type === 'ERROR') {
|
|
647
|
-
reject(new Error(mock.data));
|
|
648
|
-
} else {
|
|
649
|
-
resolve(mock.data);
|
|
650
|
-
}
|
|
651
|
-
}, mock.delayMs || 20);
|
|
652
|
-
} else {
|
|
653
|
-
console.warn('[Mock IPC] No mock for action:', action, '\u2014 returning empty SUCCESS');
|
|
654
|
-
setTimeout(() => resolve(null), 20);
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
// Safe fallback bridge for hybrid webviews (Photino / CEF / WebView2)
|
|
661
|
-
try {
|
|
662
|
-
if (!window.external) {
|
|
663
|
-
(window as any).external = {};
|
|
664
|
-
}
|
|
665
|
-
(window.external as any).sendMessage = (msg) => {
|
|
666
|
-
try {
|
|
667
|
-
const parsed = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
668
|
-
const action = parsed.Action || parsed.action;
|
|
669
|
-
const id = parsed.Id || parsed.id;
|
|
670
|
-
const mock = (window as any).__visualRunnerMocks[action];
|
|
671
|
-
|
|
672
|
-
if (mock) {
|
|
673
|
-
const response = { Id: id, Type: mock.type || 'SUCCESS', Data: mock.data };
|
|
674
|
-
setTimeout(() => {
|
|
675
|
-
const cb = (window as any).__mockCallback;
|
|
676
|
-
if (typeof cb === 'function') cb(JSON.stringify(response));
|
|
677
|
-
}, mock.delayMs || 20);
|
|
678
|
-
} else {
|
|
679
|
-
setTimeout(() => {
|
|
680
|
-
const cb = (window as any).__mockCallback;
|
|
681
|
-
if (typeof cb === 'function') cb(JSON.stringify({ Id: id, Type: 'SUCCESS', Data: null }));
|
|
682
|
-
}, 20);
|
|
683
|
-
}
|
|
684
|
-
} catch (e) {
|
|
685
|
-
console.error('[Mock IPC] Failed to process message:', e);
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
(window.external as any).receiveMessage = (callback) => {
|
|
690
|
-
(window as any).__mockCallback = callback;
|
|
691
|
-
};
|
|
692
|
-
} catch {
|
|
693
|
-
// Ignored if window.external is read-only in strict Chromium sandboxes
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys((window as any).__visualRunnerMocks).length, 'mocked actions');
|
|
697
|
-
})();
|
|
698
|
-
|
|
699
|
-
`;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// src/shared/lib/config.ts
|
|
703
|
-
var import_path4 = __toESM(require("path"));
|
|
704
|
-
var import_fs4 = __toESM(require("fs"));
|
|
705
|
-
function loadConfig(cwd = process.cwd()) {
|
|
706
|
-
const jsonConfigPath = import_path4.default.join(cwd, "agent-lens.json");
|
|
707
|
-
if (import_fs4.default.existsSync(jsonConfigPath)) {
|
|
708
|
-
try {
|
|
709
|
-
const raw = import_fs4.default.readFileSync(jsonConfigPath, "utf-8");
|
|
710
|
-
return JSON.parse(raw);
|
|
711
|
-
} catch (e) {
|
|
712
|
-
console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
const packageJsonPath = import_path4.default.join(cwd, "package.json");
|
|
716
|
-
if (import_fs4.default.existsSync(packageJsonPath)) {
|
|
717
|
-
try {
|
|
718
|
-
const raw = import_fs4.default.readFileSync(packageJsonPath, "utf-8");
|
|
719
|
-
const pkg = JSON.parse(raw);
|
|
720
|
-
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
721
|
-
return pkg.agentLens;
|
|
722
|
-
}
|
|
723
|
-
} catch {
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return {};
|
|
727
|
-
}
|
|
728
|
-
function detectStartCwd(providedCwd) {
|
|
729
|
-
if (providedCwd) {
|
|
730
|
-
const resolved = import_path4.default.resolve(process.cwd(), providedCwd);
|
|
731
|
-
if (import_fs4.default.existsSync(resolved)) {
|
|
732
|
-
return providedCwd;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
const rootPkgPath = import_path4.default.join(process.cwd(), "package.json");
|
|
736
|
-
let rootHasDevScript = false;
|
|
737
|
-
if (import_fs4.default.existsSync(rootPkgPath)) {
|
|
738
|
-
try {
|
|
739
|
-
const rootPkg = JSON.parse(import_fs4.default.readFileSync(rootPkgPath, "utf-8"));
|
|
740
|
-
rootHasDevScript = Boolean(rootPkg.scripts?.dev || rootPkg.scripts?.start);
|
|
741
|
-
} catch {
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (rootHasDevScript) {
|
|
745
|
-
return void 0;
|
|
746
|
-
}
|
|
747
|
-
const candidates = ["Frontend", "frontend", "client", "web", "ui", "apps/web", "src/frontend"];
|
|
748
|
-
for (const candidate of candidates) {
|
|
749
|
-
const candidatePkg = import_path4.default.join(process.cwd(), candidate, "package.json");
|
|
750
|
-
if (import_fs4.default.existsSync(candidatePkg)) {
|
|
751
|
-
return `./${candidate}`;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
return void 0;
|
|
755
|
-
}
|
|
756
|
-
function resolveWwwrootDir(customDir) {
|
|
757
|
-
if (customDir) {
|
|
758
|
-
return import_path4.default.resolve(process.cwd(), customDir);
|
|
759
|
-
}
|
|
760
|
-
const candidates = [
|
|
761
|
-
"dist",
|
|
762
|
-
"build",
|
|
763
|
-
"out",
|
|
764
|
-
"wwwroot",
|
|
765
|
-
"Frontend/dist",
|
|
766
|
-
"frontend/dist",
|
|
767
|
-
"client/dist"
|
|
768
|
-
];
|
|
769
|
-
for (const c of candidates) {
|
|
770
|
-
const candidatePath = import_path4.default.resolve(process.cwd(), c);
|
|
771
|
-
if (import_fs4.default.existsSync(candidatePath) && import_fs4.default.existsSync(import_path4.default.join(candidatePath, "index.html"))) {
|
|
772
|
-
return candidatePath;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
for (const c of candidates) {
|
|
776
|
-
const candidatePath = import_path4.default.resolve(process.cwd(), c);
|
|
777
|
-
if (import_fs4.default.existsSync(candidatePath)) {
|
|
778
|
-
return candidatePath;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return import_path4.default.resolve(process.cwd(), "dist");
|
|
782
|
-
}
|
|
783
|
-
|
|
784
527
|
// src/shared/drivers/previewDriver.ts
|
|
785
528
|
var MIME_TYPES = {
|
|
786
529
|
".html": "text/html; charset=utf-8",
|
|
@@ -806,13 +549,11 @@ var PreviewDriver = class {
|
|
|
806
549
|
options;
|
|
807
550
|
_baseUrl = "";
|
|
808
551
|
initialRouteMocks = [];
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
this.options = options2 || {};
|
|
552
|
+
constructor(options) {
|
|
553
|
+
this.options = options || {};
|
|
812
554
|
if (!this.options.url) {
|
|
813
555
|
this.options.wwwrootDir = resolveWwwrootDir(this.options.wwwrootDir);
|
|
814
556
|
}
|
|
815
|
-
this.mockRegistry = new MockIpcRegistry();
|
|
816
557
|
}
|
|
817
558
|
get baseUrl() {
|
|
818
559
|
return this._baseUrl;
|
|
@@ -820,25 +561,25 @@ var PreviewDriver = class {
|
|
|
820
561
|
async start(initialViewport = { width: 1200, height: 800 }) {
|
|
821
562
|
let targetUrl = this.options.url;
|
|
822
563
|
if (!targetUrl) {
|
|
823
|
-
if (!
|
|
564
|
+
if (!import_fs4.default.existsSync(this.options.wwwrootDir)) {
|
|
824
565
|
throw new Error(`Directory not found at: ${this.options.wwwrootDir}. Please build your frontend project first or pass --url=<url>.`);
|
|
825
566
|
}
|
|
826
567
|
await new Promise((resolve, reject) => {
|
|
827
|
-
this.server =
|
|
568
|
+
this.server = import_http.default.createServer((req, res) => {
|
|
828
569
|
let reqUrl = req.url?.split("?")[0] || "/";
|
|
829
570
|
if (reqUrl === "/") reqUrl = "/index.html";
|
|
830
|
-
let safePath =
|
|
571
|
+
let safePath = import_path4.default.normalize(import_path4.default.join(this.options.wwwrootDir, reqUrl));
|
|
831
572
|
if (!safePath.startsWith(this.options.wwwrootDir)) {
|
|
832
573
|
res.writeHead(403);
|
|
833
574
|
return res.end("Forbidden");
|
|
834
575
|
}
|
|
835
|
-
if (!
|
|
836
|
-
safePath =
|
|
576
|
+
if (!import_fs4.default.existsSync(safePath) || import_fs4.default.statSync(safePath).isDirectory()) {
|
|
577
|
+
safePath = import_path4.default.join(this.options.wwwrootDir, "index.html");
|
|
837
578
|
}
|
|
838
|
-
const ext =
|
|
579
|
+
const ext = import_path4.default.extname(safePath).toLowerCase();
|
|
839
580
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
840
581
|
try {
|
|
841
|
-
const content =
|
|
582
|
+
const content = import_fs4.default.readFileSync(safePath);
|
|
842
583
|
res.writeHead(200, { "Content-Type": contentType });
|
|
843
584
|
res.end(content);
|
|
844
585
|
} catch (e) {
|
|
@@ -870,10 +611,6 @@ var PreviewDriver = class {
|
|
|
870
611
|
viewport: initialViewport,
|
|
871
612
|
deviceScaleFactor: 1
|
|
872
613
|
});
|
|
873
|
-
if (this.mockRegistry.size > 0) {
|
|
874
|
-
const mockScript = generateMockIpcScript(this.mockRegistry);
|
|
875
|
-
await this.context.addInitScript(mockScript);
|
|
876
|
-
}
|
|
877
614
|
this.page = await this.context.newPage();
|
|
878
615
|
if (this.initialRouteMocks.length > 0) {
|
|
879
616
|
for (const entry of this.initialRouteMocks) {
|
|
@@ -910,28 +647,6 @@ var PreviewDriver = class {
|
|
|
910
647
|
await this.addRouteMock(r);
|
|
911
648
|
}
|
|
912
649
|
}
|
|
913
|
-
async updateMockIpc(action, data, options2) {
|
|
914
|
-
if (!this.page) {
|
|
915
|
-
throw new Error("PreviewDriver not started. Call start() first.");
|
|
916
|
-
}
|
|
917
|
-
this.mockRegistry.set(action, data, options2);
|
|
918
|
-
await this.page.evaluate(
|
|
919
|
-
({ action: action2, mock }) => {
|
|
920
|
-
if (!window.__visualRunnerMocks) {
|
|
921
|
-
window.__visualRunnerMocks = {};
|
|
922
|
-
}
|
|
923
|
-
window.__visualRunnerMocks[action2] = mock;
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
action,
|
|
927
|
-
mock: {
|
|
928
|
-
data,
|
|
929
|
-
type: options2?.type ?? "SUCCESS",
|
|
930
|
-
delayMs: options2?.delayMs ?? 20
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
650
|
async stop() {
|
|
936
651
|
if (this.context) {
|
|
937
652
|
await this.context.close().catch(() => {
|
|
@@ -953,17 +668,17 @@ var PreviewDriver = class {
|
|
|
953
668
|
};
|
|
954
669
|
|
|
955
670
|
// src/shared/lib/processManager.ts
|
|
956
|
-
var
|
|
957
|
-
var
|
|
671
|
+
var import_child_process = require("child_process");
|
|
672
|
+
var import_tree_kill = __toESM(require("tree-kill"));
|
|
958
673
|
var ProcessManager = class {
|
|
959
674
|
childProcess = null;
|
|
960
675
|
isStopped = false;
|
|
961
|
-
async start(command,
|
|
962
|
-
const cwd =
|
|
676
|
+
async start(command, options) {
|
|
677
|
+
const cwd = options?.cwd || process.cwd();
|
|
963
678
|
console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
|
|
964
|
-
this.childProcess = (0,
|
|
679
|
+
this.childProcess = (0, import_child_process.spawn)(command, {
|
|
965
680
|
cwd,
|
|
966
|
-
env: { ...process.env, ...
|
|
681
|
+
env: { ...process.env, ...options?.env },
|
|
967
682
|
shell: true,
|
|
968
683
|
stdio: ["ignore", "pipe", "pipe"]
|
|
969
684
|
});
|
|
@@ -1012,10 +727,10 @@ var ProcessManager = class {
|
|
|
1012
727
|
const pid = this.childProcess.pid;
|
|
1013
728
|
console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
|
|
1014
729
|
await new Promise((resolve) => {
|
|
1015
|
-
(0,
|
|
730
|
+
(0, import_tree_kill.default)(pid, "SIGTERM", (err) => {
|
|
1016
731
|
if (err) {
|
|
1017
732
|
try {
|
|
1018
|
-
(0,
|
|
733
|
+
(0, import_tree_kill.default)(pid, "SIGKILL");
|
|
1019
734
|
} catch {
|
|
1020
735
|
}
|
|
1021
736
|
}
|
|
@@ -1026,31 +741,438 @@ var ProcessManager = class {
|
|
|
1026
741
|
}
|
|
1027
742
|
};
|
|
1028
743
|
|
|
1029
|
-
// src/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
744
|
+
// src/shared/lib/pluginLoader.ts
|
|
745
|
+
var import_path5 = __toESM(require("path"));
|
|
746
|
+
var import_fs5 = __toESM(require("fs"));
|
|
747
|
+
var import_jiti = require("jiti");
|
|
748
|
+
function findPackageRoot() {
|
|
749
|
+
let cur = __dirname;
|
|
750
|
+
while (cur !== import_path5.default.dirname(cur)) {
|
|
751
|
+
const pkgPath = import_path5.default.join(cur, "package.json");
|
|
752
|
+
if (import_fs5.default.existsSync(pkgPath)) {
|
|
753
|
+
try {
|
|
754
|
+
const pkg = JSON.parse(import_fs5.default.readFileSync(pkgPath, "utf-8"));
|
|
755
|
+
if (pkg.name === "@_deep4wee/agent-lens" || pkg.name === "agent-lens") {
|
|
756
|
+
return cur;
|
|
757
|
+
}
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
cur = import_path5.default.dirname(cur);
|
|
762
|
+
}
|
|
763
|
+
return import_path5.default.resolve(__dirname, "..");
|
|
764
|
+
}
|
|
765
|
+
var PluginManager = class {
|
|
766
|
+
plugins = [];
|
|
767
|
+
jiti = (0, import_jiti.createJiti)(process.cwd());
|
|
768
|
+
register(plugin) {
|
|
769
|
+
if (this.plugins.some((p) => p.name === plugin.name)) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.plugins.push(plugin);
|
|
773
|
+
console.log(`\u{1F50C} [Plugin] Registered: ${plugin.name}${plugin.version ? ` (v${plugin.version})` : ""}`);
|
|
774
|
+
}
|
|
775
|
+
async load(pluginSpec) {
|
|
776
|
+
if (typeof pluginSpec === "object" && pluginSpec !== null) {
|
|
777
|
+
this.register(pluginSpec);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const pluginNameOrPath = pluginSpec.trim();
|
|
781
|
+
if (!pluginNameOrPath) return;
|
|
782
|
+
if (pluginNameOrPath.startsWith(".") || pluginNameOrPath.startsWith("/") || pluginNameOrPath.includes("/") || pluginNameOrPath.includes("\\")) {
|
|
783
|
+
const resolvedPath = import_path5.default.resolve(process.cwd(), pluginNameOrPath);
|
|
784
|
+
await this.loadFromFile(resolvedPath);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const localCandidates = [
|
|
788
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.ts`),
|
|
789
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", `${pluginNameOrPath}.js`),
|
|
790
|
+
import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.ts`),
|
|
791
|
+
import_path5.default.resolve(process.cwd(), "plugins", `${pluginNameOrPath}.js`),
|
|
792
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.ts"),
|
|
793
|
+
import_path5.default.resolve(process.cwd(), ".agent-lens", "plugins", pluginNameOrPath, "index.js"),
|
|
794
|
+
import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.ts"),
|
|
795
|
+
import_path5.default.resolve(process.cwd(), "plugins", pluginNameOrPath, "index.js")
|
|
796
|
+
];
|
|
797
|
+
for (const candidate of localCandidates) {
|
|
798
|
+
if (import_fs5.default.existsSync(candidate)) {
|
|
799
|
+
await this.loadFromFile(candidate);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const pkgRoot = findPackageRoot();
|
|
804
|
+
const builtInCandidates = [
|
|
805
|
+
import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.ts"),
|
|
806
|
+
import_path5.default.join(pkgRoot, "src", "plugins", pluginNameOrPath, "index.js"),
|
|
807
|
+
import_path5.default.join(pkgRoot, "dist", "plugins", pluginNameOrPath, "index.js"),
|
|
808
|
+
import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.ts"),
|
|
809
|
+
import_path5.default.join(pkgRoot, "plugins", pluginNameOrPath, "index.js"),
|
|
810
|
+
import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.ts"),
|
|
811
|
+
import_path5.default.resolve(__dirname, "../../plugins", pluginNameOrPath, "index.js"),
|
|
812
|
+
import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.ts"),
|
|
813
|
+
import_path5.default.resolve(__dirname, "../plugins", pluginNameOrPath, "index.js")
|
|
814
|
+
];
|
|
815
|
+
for (const builtIn of builtInCandidates) {
|
|
816
|
+
if (import_fs5.default.existsSync(builtIn)) {
|
|
817
|
+
await this.loadFromFile(builtIn);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const mod = await this.jiti.import(pluginNameOrPath);
|
|
823
|
+
const plugin = mod.default || mod.plugin || mod;
|
|
824
|
+
if (plugin && plugin.name) {
|
|
825
|
+
this.register(plugin);
|
|
826
|
+
} else {
|
|
827
|
+
console.warn(`\u26A0\uFE0F [Plugin] Package "${pluginNameOrPath}" did not export a valid AgentLensPlugin.`);
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.warn(`\u26A0\uFE0F [Plugin] Could not load plugin '${pluginNameOrPath}': ${err.message}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async loadFromFile(filePath) {
|
|
834
|
+
if (!import_fs5.default.existsSync(filePath)) {
|
|
835
|
+
console.warn(`\u26A0\uFE0F [Plugin] Plugin file not found: ${filePath}`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const mod = await this.jiti.import(filePath);
|
|
840
|
+
const plugin = mod.default || mod.plugin || mod;
|
|
841
|
+
if (plugin && plugin.name) {
|
|
842
|
+
this.register(plugin);
|
|
843
|
+
} else {
|
|
844
|
+
console.warn(`\u26A0\uFE0F [Plugin] File "${filePath}" does not export a valid AgentLensPlugin by default.`);
|
|
845
|
+
}
|
|
846
|
+
} catch (err) {
|
|
847
|
+
console.error(`\u274C [Plugin] Failed to import plugin from ${filePath}:`, err.message);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async loadAll(specs) {
|
|
851
|
+
for (const spec of specs) {
|
|
852
|
+
await this.load(spec);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async runSetup(hookContext) {
|
|
856
|
+
for (const p of this.plugins) {
|
|
857
|
+
if (p.setup) await p.setup(hookContext);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async launchSession(options, hookContext) {
|
|
861
|
+
for (const p of this.plugins) {
|
|
862
|
+
if (p.launchSession) {
|
|
863
|
+
const session = await p.launchSession(options, hookContext);
|
|
864
|
+
if (session) {
|
|
865
|
+
return session;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return void 0;
|
|
870
|
+
}
|
|
871
|
+
async runOnContextCreated(context, hookContext) {
|
|
872
|
+
for (const p of this.plugins) {
|
|
873
|
+
if (p.onContextCreated) await p.onContextCreated(context, hookContext);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async runOnPageCreated(page, context, hookContext) {
|
|
877
|
+
for (const p of this.plugins) {
|
|
878
|
+
if (p.onPageCreated) await p.onPageCreated(page, context, hookContext);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async extendContext(ctx, page, hookContext) {
|
|
882
|
+
for (const p of this.plugins) {
|
|
883
|
+
if (p.extendContext) {
|
|
884
|
+
const extensions = await p.extendContext(ctx, page, hookContext);
|
|
885
|
+
if (extensions && typeof extensions === "object") {
|
|
886
|
+
Object.assign(ctx, extensions);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async runOnAfterRun(reportData, hookContext) {
|
|
892
|
+
for (const p of this.plugins) {
|
|
893
|
+
if (p.onAfterRun) await p.onAfterRun(reportData, hookContext);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async runTeardown(hookContext) {
|
|
897
|
+
for (const p of this.plugins) {
|
|
898
|
+
if (p.teardown) {
|
|
899
|
+
try {
|
|
900
|
+
await p.teardown(hookContext);
|
|
901
|
+
} catch (e) {
|
|
902
|
+
console.error(`\u26A0\uFE0F [Plugin] Error in ${p.name}.teardown:`, e.message);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
get loadedPlugins() {
|
|
908
|
+
return [...this.plugins];
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/features/runner/lib/navigation.ts
|
|
913
|
+
function createNavigator(page, baseUrl, targetUrl) {
|
|
914
|
+
return async (route) => {
|
|
915
|
+
if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
916
|
+
await page.goto(route, { waitUntil: "domcontentloaded" });
|
|
917
|
+
await page.waitForTimeout(300);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (route.startsWith("#")) {
|
|
921
|
+
await page.evaluate((r) => {
|
|
922
|
+
window.location.hash = r;
|
|
923
|
+
}, route);
|
|
924
|
+
await page.waitForTimeout(300);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (baseUrl && targetUrl) {
|
|
928
|
+
try {
|
|
929
|
+
const fullUrl = new URL(route, baseUrl).toString();
|
|
930
|
+
await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
|
|
931
|
+
await page.waitForTimeout(300);
|
|
932
|
+
return;
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const routePath = route.startsWith("/") ? route : `/${route}`;
|
|
937
|
+
await page.evaluate((r) => {
|
|
938
|
+
if (window.location.hash !== void 0) {
|
|
939
|
+
window.location.hash = r;
|
|
940
|
+
}
|
|
941
|
+
}, routePath);
|
|
942
|
+
await page.waitForTimeout(300);
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/features/runner/lib/artifactsSync.ts
|
|
947
|
+
var import_fs6 = __toESM(require("fs"));
|
|
948
|
+
var import_path6 = __toESM(require("path"));
|
|
949
|
+
function syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots) {
|
|
950
|
+
try {
|
|
951
|
+
const latestDir = import_path6.default.join(artifactsRoot, "latest");
|
|
952
|
+
if (import_fs6.default.existsSync(latestDir)) {
|
|
953
|
+
import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
|
|
954
|
+
}
|
|
955
|
+
import_fs6.default.mkdirSync(latestDir, { recursive: true });
|
|
956
|
+
if (import_fs6.default.existsSync(reportPath)) {
|
|
957
|
+
import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
|
|
958
|
+
}
|
|
959
|
+
const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
|
|
960
|
+
if (import_fs6.default.existsSync(manifestSrc)) {
|
|
961
|
+
import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
|
|
962
|
+
}
|
|
963
|
+
for (const snap of snapshots) {
|
|
964
|
+
if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
|
|
965
|
+
import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return import_path6.default.join(latestDir, "report.md");
|
|
969
|
+
} catch {
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/features/runner/lib/contextBuilder.ts
|
|
975
|
+
function buildTestContext(options) {
|
|
976
|
+
const {
|
|
977
|
+
page,
|
|
978
|
+
context,
|
|
979
|
+
targetMode,
|
|
980
|
+
initialViewport,
|
|
981
|
+
captureEngine,
|
|
982
|
+
consoleTracker,
|
|
983
|
+
previewDriver,
|
|
984
|
+
doNavigate
|
|
985
|
+
} = options;
|
|
986
|
+
let currentViewport = { ...initialViewport };
|
|
987
|
+
const ctx = {
|
|
988
|
+
page,
|
|
989
|
+
context,
|
|
990
|
+
targetMode,
|
|
991
|
+
currentViewport,
|
|
992
|
+
// --- Snapshots ---
|
|
993
|
+
capture: async (name, opts) => {
|
|
994
|
+
console.log(`\u{1F4F8} [Snapshot] ${name} (${currentViewport.width}x${currentViewport.height})`);
|
|
995
|
+
return await captureEngine.takeSnapshot(page, name, currentViewport, opts);
|
|
996
|
+
},
|
|
997
|
+
captureBurst: async (name, opts) => {
|
|
998
|
+
console.log(`\u{1F3AC} [Burst] ${name} (duration: ${opts.durationMs}ms, interval: ${opts.intervalMs ?? 80}ms)`);
|
|
999
|
+
return await captureEngine.takeBurst(page, name, currentViewport, opts);
|
|
1000
|
+
},
|
|
1001
|
+
// --- Navigation ---
|
|
1002
|
+
navigate: async (route) => {
|
|
1003
|
+
console.log(`\u{1F9ED} [Navigate] ${route}`);
|
|
1004
|
+
await doNavigate(route);
|
|
1005
|
+
},
|
|
1006
|
+
// --- Viewport Management ---
|
|
1007
|
+
resize: async (width, height) => {
|
|
1008
|
+
console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
|
|
1009
|
+
currentViewport = { width, height };
|
|
1010
|
+
ctx.currentViewport = currentViewport;
|
|
1011
|
+
await page.setViewportSize(currentViewport);
|
|
1012
|
+
await page.waitForTimeout(200);
|
|
1013
|
+
},
|
|
1014
|
+
setPreset: async (preset) => {
|
|
1015
|
+
console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
|
|
1016
|
+
await ctx.resize(preset.width, preset.height);
|
|
1017
|
+
},
|
|
1018
|
+
resizeToFit: async (selector, padding = 0) => {
|
|
1019
|
+
let boundingBox;
|
|
1020
|
+
if (selector) {
|
|
1021
|
+
const el = await page.$(selector);
|
|
1022
|
+
if (el) {
|
|
1023
|
+
boundingBox = await el.boundingBox();
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
boundingBox = await page.evaluate(() => ({
|
|
1027
|
+
width: document.documentElement.scrollWidth,
|
|
1028
|
+
height: document.documentElement.scrollHeight
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
1031
|
+
if (boundingBox) {
|
|
1032
|
+
const newWidth = Math.ceil(boundingBox.width) + padding * 2;
|
|
1033
|
+
const newHeight = Math.ceil(boundingBox.height) + padding * 2;
|
|
1034
|
+
console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
|
|
1035
|
+
await ctx.resize(newWidth, newHeight);
|
|
1036
|
+
} else {
|
|
1037
|
+
console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
// --- Waiting & DOM Observation ---
|
|
1041
|
+
wait: async (ms) => {
|
|
1042
|
+
await page.waitForTimeout(ms);
|
|
1043
|
+
},
|
|
1044
|
+
waitForSelector: async (selector, timeoutMs = 5e3) => {
|
|
1045
|
+
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1046
|
+
},
|
|
1047
|
+
// --- Interaction ---
|
|
1048
|
+
click: async (selector) => {
|
|
1049
|
+
console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
|
|
1050
|
+
await page.click(selector);
|
|
1051
|
+
},
|
|
1052
|
+
rightClick: async (selector) => {
|
|
1053
|
+
console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
|
|
1054
|
+
await page.click(selector, { button: "right" });
|
|
1055
|
+
},
|
|
1056
|
+
type: async (selector, text) => {
|
|
1057
|
+
console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
|
|
1058
|
+
await page.fill(selector, text);
|
|
1059
|
+
},
|
|
1060
|
+
selectOption: async (selector, value) => {
|
|
1061
|
+
console.log(`\u2705 [Select] ${selector} -> "${value}"`);
|
|
1062
|
+
await page.selectOption(selector, value);
|
|
1063
|
+
},
|
|
1064
|
+
hover: async (selector) => {
|
|
1065
|
+
console.log(`\u{1F446} [Hover] ${selector}`);
|
|
1066
|
+
await page.hover(selector);
|
|
1067
|
+
},
|
|
1068
|
+
scroll: async (selector, deltaY) => {
|
|
1069
|
+
console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
|
|
1070
|
+
await page.evaluate(
|
|
1071
|
+
({ sel, dY }) => {
|
|
1072
|
+
const el = document.querySelector(sel);
|
|
1073
|
+
if (el) {
|
|
1074
|
+
el.scrollTop += dY;
|
|
1075
|
+
} else {
|
|
1076
|
+
window.scrollBy(0, dY);
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
{ sel: selector, dY: deltaY }
|
|
1080
|
+
);
|
|
1081
|
+
await page.waitForTimeout(100);
|
|
1082
|
+
},
|
|
1083
|
+
// --- Logging ---
|
|
1084
|
+
log: (msg) => {
|
|
1085
|
+
console.log(`\u2139\uFE0F [Scenario] ${msg}`);
|
|
1086
|
+
},
|
|
1087
|
+
// --- Mock IPC (delegated or stubbed if plugin not loaded) ---
|
|
1088
|
+
setMockIpc: async (action, data, mockOptions) => {
|
|
1089
|
+
console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc called for "${action}". Ensure --plugin=mock-ipc is enabled.`);
|
|
1090
|
+
},
|
|
1091
|
+
// --- HTTP Route Mocking ---
|
|
1092
|
+
setMockRoute: async (url, body, routeOptions) => {
|
|
1093
|
+
if (!previewDriver) {
|
|
1094
|
+
console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored (no preview driver running)`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
|
|
1098
|
+
await previewDriver.addRouteMock({
|
|
1099
|
+
url,
|
|
1100
|
+
body,
|
|
1101
|
+
method: routeOptions?.method,
|
|
1102
|
+
status: routeOptions?.status,
|
|
1103
|
+
delayMs: routeOptions?.delayMs,
|
|
1104
|
+
headers: routeOptions?.headers
|
|
1105
|
+
});
|
|
1106
|
+
},
|
|
1107
|
+
// --- Console Errors ---
|
|
1108
|
+
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1109
|
+
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1110
|
+
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
1111
|
+
// --- DOM Assertions ---
|
|
1112
|
+
readText: async (selector) => {
|
|
1113
|
+
const text = await page.textContent(selector);
|
|
1114
|
+
return text ? text.trim() : null;
|
|
1115
|
+
},
|
|
1116
|
+
getPageText: async () => {
|
|
1117
|
+
return await page.evaluate(() => document.body.innerText || "");
|
|
1118
|
+
},
|
|
1119
|
+
isVisible: async (selector) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const element = await page.$(selector);
|
|
1122
|
+
if (!element) return false;
|
|
1123
|
+
return await element.isVisible();
|
|
1124
|
+
} catch {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
getElementCount: async (selector) => {
|
|
1129
|
+
const elements = await page.$$(selector);
|
|
1130
|
+
return elements.length;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
return {
|
|
1134
|
+
ctx,
|
|
1135
|
+
getCurrentViewport: () => currentViewport
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/features/runner/runner.ts
|
|
1140
|
+
async function runVisualScenario(options) {
|
|
1141
|
+
const { scenario } = options;
|
|
1142
|
+
const targetMode = options.targetMode || "preview";
|
|
1033
1143
|
const startTime = Date.now();
|
|
1034
1144
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1035
|
-
const artifactsRoot =
|
|
1036
|
-
if (
|
|
1145
|
+
const artifactsRoot = options.artifactsRoot || import_path7.default.resolve(process.cwd(), "artifacts");
|
|
1146
|
+
if (options.cleanArtifacts && import_fs7.default.existsSync(artifactsRoot)) {
|
|
1037
1147
|
console.log(`\u{1F9F9} [Clean Artifacts] Purging previous artifact runs in ${artifactsRoot}...`);
|
|
1038
|
-
const entries =
|
|
1148
|
+
const entries = import_fs7.default.readdirSync(artifactsRoot, { withFileTypes: true });
|
|
1039
1149
|
for (const entry of entries) {
|
|
1040
|
-
|
|
1150
|
+
import_fs7.default.rmSync(import_path7.default.join(artifactsRoot, entry.name), { recursive: true, force: true });
|
|
1041
1151
|
}
|
|
1042
1152
|
}
|
|
1043
|
-
const scenarioArtifactsDir =
|
|
1044
|
-
if (!
|
|
1045
|
-
|
|
1153
|
+
const scenarioArtifactsDir = import_path7.default.join(artifactsRoot, `${scenario.id}_${timestamp}`);
|
|
1154
|
+
if (!import_fs7.default.existsSync(scenarioArtifactsDir)) {
|
|
1155
|
+
import_fs7.default.mkdirSync(scenarioArtifactsDir, { recursive: true });
|
|
1046
1156
|
}
|
|
1047
1157
|
const captureEngine = new CaptureEngine(scenarioArtifactsDir);
|
|
1048
1158
|
const consoleTracker = new ConsoleTracker();
|
|
1049
1159
|
const defaultViewport = scenario.viewports?.[0] || VIEWPORT_PRESETS.DEFAULT;
|
|
1050
|
-
|
|
1051
|
-
let desktopDriver = null;
|
|
1160
|
+
const currentViewport = { width: defaultViewport.width, height: defaultViewport.height };
|
|
1052
1161
|
let previewDriver = null;
|
|
1053
1162
|
let processManager = null;
|
|
1163
|
+
let customDriverStop = null;
|
|
1164
|
+
const pluginManager = new PluginManager();
|
|
1165
|
+
const pluginSpecs = [...options.plugins || [], ...scenario.plugins || []];
|
|
1166
|
+
if (pluginSpecs.length > 0) {
|
|
1167
|
+
await pluginManager.loadAll(pluginSpecs);
|
|
1168
|
+
}
|
|
1169
|
+
const hookContext = {
|
|
1170
|
+
scenario,
|
|
1171
|
+
targetMode,
|
|
1172
|
+
artifactsDir: scenarioArtifactsDir,
|
|
1173
|
+
cliOptions: options,
|
|
1174
|
+
state: /* @__PURE__ */ new Map()
|
|
1175
|
+
};
|
|
1054
1176
|
try {
|
|
1055
1177
|
let page;
|
|
1056
1178
|
let context;
|
|
@@ -1061,38 +1183,37 @@ async function runVisualScenario(options2) {
|
|
|
1061
1183
|
console.log(`\u{1F4C1} Artifacts: ${scenarioArtifactsDir}`);
|
|
1062
1184
|
console.log(`========================================
|
|
1063
1185
|
`);
|
|
1064
|
-
if (
|
|
1186
|
+
if (options.startCommand) {
|
|
1065
1187
|
processManager = new ProcessManager();
|
|
1066
|
-
await processManager.start(
|
|
1067
|
-
const waitTarget =
|
|
1188
|
+
await processManager.start(options.startCommand, { cwd: options.startCwd });
|
|
1189
|
+
const waitTarget = options.url || "http://localhost:5173";
|
|
1068
1190
|
await processManager.waitForUrl(waitTarget);
|
|
1069
1191
|
}
|
|
1192
|
+
await pluginManager.runSetup(hookContext);
|
|
1070
1193
|
if (typeof scenario.setup === "function") {
|
|
1071
1194
|
console.log(`\u{1F527} [Scenario Setup] Executing setup hook...`);
|
|
1072
1195
|
await scenario.setup();
|
|
1073
1196
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1197
|
+
const customSession = await pluginManager.launchSession(
|
|
1198
|
+
{ currentViewport, headed: options.headed },
|
|
1199
|
+
hookContext
|
|
1200
|
+
);
|
|
1201
|
+
if (customSession) {
|
|
1202
|
+
page = customSession.page;
|
|
1203
|
+
context = customSession.context;
|
|
1204
|
+
if (customSession.stop) {
|
|
1205
|
+
customDriverStop = customSession.stop;
|
|
1206
|
+
}
|
|
1207
|
+
} else if (targetMode === "desktop") {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`[AgentLens] Target mode is 'desktop', but no desktop plugin is registered. Please add --plugin=desktop-webview2 to your command or configuration.`
|
|
1210
|
+
);
|
|
1085
1211
|
} else {
|
|
1086
1212
|
previewDriver = new PreviewDriver({
|
|
1087
|
-
wwwrootDir:
|
|
1088
|
-
url:
|
|
1089
|
-
headed:
|
|
1213
|
+
wwwrootDir: options.wwwrootDir,
|
|
1214
|
+
url: options.url,
|
|
1215
|
+
headed: options.headed
|
|
1090
1216
|
});
|
|
1091
|
-
const mergedMocks = [...options2.globalMocks || [], ...scenario.mockIpc || []];
|
|
1092
|
-
if (mergedMocks.length > 0) {
|
|
1093
|
-
console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
|
|
1094
|
-
previewDriver.mockRegistry.setBatch(mergedMocks);
|
|
1095
|
-
}
|
|
1096
1217
|
if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
|
|
1097
1218
|
console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
|
|
1098
1219
|
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
@@ -1104,194 +1225,27 @@ async function runVisualScenario(options2) {
|
|
|
1104
1225
|
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
1105
1226
|
}
|
|
1106
1227
|
}
|
|
1228
|
+
await pluginManager.runOnContextCreated(context, hookContext);
|
|
1229
|
+
await pluginManager.runOnPageCreated(page, context, hookContext);
|
|
1107
1230
|
consoleTracker.attach(page);
|
|
1108
1231
|
console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
|
|
1109
1232
|
`);
|
|
1110
|
-
const doNavigate =
|
|
1111
|
-
if (route.startsWith("http://") || route.startsWith("https://")) {
|
|
1112
|
-
await page.goto(route, { waitUntil: "domcontentloaded" });
|
|
1113
|
-
await page.waitForTimeout(300);
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
if (route.startsWith("#")) {
|
|
1117
|
-
await page.evaluate((r) => {
|
|
1118
|
-
window.location.hash = r;
|
|
1119
|
-
}, route);
|
|
1120
|
-
await page.waitForTimeout(300);
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
if (previewDriver?.baseUrl && options2.url) {
|
|
1124
|
-
try {
|
|
1125
|
-
const fullUrl = new URL(route, previewDriver.baseUrl).toString();
|
|
1126
|
-
await page.goto(fullUrl, { waitUntil: "domcontentloaded" });
|
|
1127
|
-
await page.waitForTimeout(300);
|
|
1128
|
-
return;
|
|
1129
|
-
} catch {
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
const routePath = route.startsWith("/") ? route : `/${route}`;
|
|
1133
|
-
await page.evaluate((r) => {
|
|
1134
|
-
if (window.location.hash !== void 0) {
|
|
1135
|
-
window.location.hash = r;
|
|
1136
|
-
}
|
|
1137
|
-
}, routePath);
|
|
1138
|
-
await page.waitForTimeout(300);
|
|
1139
|
-
};
|
|
1233
|
+
const doNavigate = createNavigator(page, previewDriver?.baseUrl, options.url);
|
|
1140
1234
|
if (scenario.route) {
|
|
1141
|
-
console.log(
|
|
1235
|
+
console.log(`\u{1F9ED} [Runner] Navigating to initial route: ${scenario.route}`);
|
|
1142
1236
|
await doNavigate(scenario.route);
|
|
1143
1237
|
}
|
|
1144
|
-
const ctx = {
|
|
1238
|
+
const { ctx } = buildTestContext({
|
|
1145
1239
|
page,
|
|
1146
1240
|
context,
|
|
1147
1241
|
targetMode,
|
|
1148
|
-
currentViewport,
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
return await captureEngine.takeBurst(page, name, currentViewport, opts);
|
|
1156
|
-
},
|
|
1157
|
-
navigate: async (route) => {
|
|
1158
|
-
console.log(`\u{1F9ED} [Navigate] ${route}`);
|
|
1159
|
-
await doNavigate(route);
|
|
1160
|
-
},
|
|
1161
|
-
// ─── Viewport ───
|
|
1162
|
-
resize: async (width, height) => {
|
|
1163
|
-
console.log(`\u{1F4D0} [Resize] ${width}x${height}`);
|
|
1164
|
-
currentViewport = { width, height };
|
|
1165
|
-
ctx.currentViewport = currentViewport;
|
|
1166
|
-
await page.setViewportSize(currentViewport);
|
|
1167
|
-
await page.waitForTimeout(200);
|
|
1168
|
-
},
|
|
1169
|
-
setPreset: async (preset) => {
|
|
1170
|
-
console.log(`\u{1F4D0} [Preset] ${preset.name} (${preset.width}x${preset.height})`);
|
|
1171
|
-
await ctx.resize(preset.width, preset.height);
|
|
1172
|
-
},
|
|
1173
|
-
resizeToFit: async (selector, padding = 0) => {
|
|
1174
|
-
let boundingBox;
|
|
1175
|
-
if (selector) {
|
|
1176
|
-
const el = await page.$(selector);
|
|
1177
|
-
if (el) {
|
|
1178
|
-
boundingBox = await el.boundingBox();
|
|
1179
|
-
}
|
|
1180
|
-
} else {
|
|
1181
|
-
boundingBox = await page.evaluate(() => {
|
|
1182
|
-
return {
|
|
1183
|
-
width: document.documentElement.scrollWidth,
|
|
1184
|
-
height: document.documentElement.scrollHeight
|
|
1185
|
-
};
|
|
1186
|
-
});
|
|
1187
|
-
}
|
|
1188
|
-
if (boundingBox) {
|
|
1189
|
-
const newWidth = Math.ceil(boundingBox.width) + padding * 2;
|
|
1190
|
-
const newHeight = Math.ceil(boundingBox.height) + padding * 2;
|
|
1191
|
-
console.log(`\u{1F4D0} [ResizeToFit] ${selector || "body"} -> ${newWidth}x${newHeight}`);
|
|
1192
|
-
await ctx.resize(newWidth, newHeight);
|
|
1193
|
-
} else {
|
|
1194
|
-
console.log(`\u26A0\uFE0F [ResizeToFit] Element ${selector} not found or has no bounding box.`);
|
|
1195
|
-
}
|
|
1196
|
-
},
|
|
1197
|
-
wait: async (ms) => {
|
|
1198
|
-
await page.waitForTimeout(ms);
|
|
1199
|
-
},
|
|
1200
|
-
waitForSelector: async (selector, timeoutMs = 5e3) => {
|
|
1201
|
-
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
1202
|
-
},
|
|
1203
|
-
click: async (selector) => {
|
|
1204
|
-
console.log(`\u{1F5B1}\uFE0F [Click] ${selector}`);
|
|
1205
|
-
await page.click(selector);
|
|
1206
|
-
},
|
|
1207
|
-
rightClick: async (selector) => {
|
|
1208
|
-
console.log(`\u{1F5B1}\uFE0F [RightClick] ${selector}`);
|
|
1209
|
-
await page.click(selector, { button: "right" });
|
|
1210
|
-
},
|
|
1211
|
-
type: async (selector, text) => {
|
|
1212
|
-
console.log(`\u2328\uFE0F [Type] ${selector} -> "${text}"`);
|
|
1213
|
-
await page.fill(selector, text);
|
|
1214
|
-
},
|
|
1215
|
-
selectOption: async (selector, value) => {
|
|
1216
|
-
console.log(`\u2705 [Select] ${selector} -> "${value}"`);
|
|
1217
|
-
await page.selectOption(selector, value);
|
|
1218
|
-
},
|
|
1219
|
-
hover: async (selector) => {
|
|
1220
|
-
console.log(`\u{1F446} [Hover] ${selector}`);
|
|
1221
|
-
await page.hover(selector);
|
|
1222
|
-
},
|
|
1223
|
-
scroll: async (selector, deltaY) => {
|
|
1224
|
-
console.log(`\u{1F4DC} [Scroll] ${selector} by ${deltaY}px`);
|
|
1225
|
-
await page.evaluate(({ sel, dY }) => {
|
|
1226
|
-
const el = document.querySelector(sel);
|
|
1227
|
-
if (el) {
|
|
1228
|
-
el.scrollTop += dY;
|
|
1229
|
-
} else {
|
|
1230
|
-
window.scrollBy(0, dY);
|
|
1231
|
-
}
|
|
1232
|
-
}, { sel: selector, dY: deltaY });
|
|
1233
|
-
await page.waitForTimeout(100);
|
|
1234
|
-
},
|
|
1235
|
-
log: (msg) => {
|
|
1236
|
-
console.log(`\u2139\uFE0F [Scenario] ${msg}`);
|
|
1237
|
-
},
|
|
1238
|
-
// ─── Mock IPC ───
|
|
1239
|
-
setMockIpc: async (action, data, mockOptions) => {
|
|
1240
|
-
if (targetMode === "desktop") {
|
|
1241
|
-
console.log(`\u26A0\uFE0F [Mock IPC] setMockIpc ignored in desktop mode (real backend handles IPC)`);
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
if (!previewDriver) {
|
|
1245
|
-
console.log(`\u26A0\uFE0F [Mock IPC] PreviewDriver not available`);
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
|
|
1249
|
-
await previewDriver.updateMockIpc(action, data, mockOptions);
|
|
1250
|
-
},
|
|
1251
|
-
setMockRoute: async (url, body, routeOptions) => {
|
|
1252
|
-
if (targetMode === "desktop") {
|
|
1253
|
-
console.log(`\u26A0\uFE0F [Mock Route] setMockRoute ignored in desktop mode`);
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
if (!previewDriver) {
|
|
1257
|
-
console.log(`\u26A0\uFE0F [Mock Route] PreviewDriver not available`);
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
console.log(`\u{1F310} [Mock Route] Intercepting ${routeOptions?.method || "ALL"} ${url} -> ${routeOptions?.status ?? 200}`);
|
|
1261
|
-
await previewDriver.addRouteMock({
|
|
1262
|
-
url,
|
|
1263
|
-
body,
|
|
1264
|
-
method: routeOptions?.method,
|
|
1265
|
-
status: routeOptions?.status,
|
|
1266
|
-
delayMs: routeOptions?.delayMs,
|
|
1267
|
-
headers: routeOptions?.headers
|
|
1268
|
-
});
|
|
1269
|
-
},
|
|
1270
|
-
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1271
|
-
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1272
|
-
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
1273
|
-
// ─── DOM Assertions ───
|
|
1274
|
-
readText: async (selector) => {
|
|
1275
|
-
const text = await page.textContent(selector);
|
|
1276
|
-
return text ? text.trim() : null;
|
|
1277
|
-
},
|
|
1278
|
-
getPageText: async () => {
|
|
1279
|
-
return await page.evaluate(() => document.body.innerText || "");
|
|
1280
|
-
},
|
|
1281
|
-
isVisible: async (selector) => {
|
|
1282
|
-
try {
|
|
1283
|
-
const element = await page.$(selector);
|
|
1284
|
-
if (!element) return false;
|
|
1285
|
-
return await element.isVisible();
|
|
1286
|
-
} catch {
|
|
1287
|
-
return false;
|
|
1288
|
-
}
|
|
1289
|
-
},
|
|
1290
|
-
getElementCount: async (selector) => {
|
|
1291
|
-
const elements = await page.$$(selector);
|
|
1292
|
-
return elements.length;
|
|
1293
|
-
}
|
|
1294
|
-
};
|
|
1242
|
+
initialViewport: currentViewport,
|
|
1243
|
+
captureEngine,
|
|
1244
|
+
consoleTracker,
|
|
1245
|
+
previewDriver,
|
|
1246
|
+
doNavigate
|
|
1247
|
+
});
|
|
1248
|
+
await pluginManager.extendContext(ctx, page, hookContext);
|
|
1295
1249
|
await scenario.run(ctx);
|
|
1296
1250
|
const durationMs = Date.now() - startTime;
|
|
1297
1251
|
const snapshots = captureEngine.getSnapshots();
|
|
@@ -1306,38 +1260,19 @@ async function runVisualScenario(options2) {
|
|
|
1306
1260
|
console.log(`
|
|
1307
1261
|
\u{1F7E1} Console Warnings: ${consoleWarnings.length}`);
|
|
1308
1262
|
}
|
|
1309
|
-
const
|
|
1263
|
+
const reportData = {
|
|
1310
1264
|
scenario,
|
|
1311
1265
|
snapshots,
|
|
1312
1266
|
consoleErrors,
|
|
1313
1267
|
consoleWarnings,
|
|
1314
1268
|
outputDir: scenarioArtifactsDir,
|
|
1315
1269
|
targetMode,
|
|
1316
|
-
durationMs
|
|
1317
|
-
|
|
1318
|
-
const syncLatest = (repPath, snaps) => {
|
|
1319
|
-
try {
|
|
1320
|
-
const latestDir = import_path6.default.join(artifactsRoot, "latest");
|
|
1321
|
-
if (import_fs6.default.existsSync(latestDir)) {
|
|
1322
|
-
import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
|
|
1323
|
-
}
|
|
1324
|
-
import_fs6.default.mkdirSync(latestDir, { recursive: true });
|
|
1325
|
-
import_fs6.default.copyFileSync(repPath, import_path6.default.join(latestDir, "report.md"));
|
|
1326
|
-
const manifestSrc = import_path6.default.join(scenarioArtifactsDir, "manifest.json");
|
|
1327
|
-
if (import_fs6.default.existsSync(manifestSrc)) {
|
|
1328
|
-
import_fs6.default.copyFileSync(manifestSrc, import_path6.default.join(latestDir, "manifest.json"));
|
|
1329
|
-
}
|
|
1330
|
-
for (const snap of snaps) {
|
|
1331
|
-
if (snap.filePath && import_fs6.default.existsSync(snap.filePath)) {
|
|
1332
|
-
import_fs6.default.copyFileSync(snap.filePath, import_path6.default.join(latestDir, snap.fileName));
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
return import_path6.default.join(latestDir, "report.md");
|
|
1336
|
-
} catch {
|
|
1337
|
-
return void 0;
|
|
1338
|
-
}
|
|
1270
|
+
durationMs,
|
|
1271
|
+
customSections: []
|
|
1339
1272
|
};
|
|
1340
|
-
|
|
1273
|
+
await pluginManager.runOnAfterRun(reportData, hookContext);
|
|
1274
|
+
const reportPath = VisualReporter.generateReport(reportData);
|
|
1275
|
+
const latestReport = syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
|
|
1341
1276
|
console.log(`
|
|
1342
1277
|
\u2705 Visual Test Completed Successfully!`);
|
|
1343
1278
|
console.log(`\u{1F4CA} Captured Snapshots: ${snapshots.length}`);
|
|
@@ -1364,7 +1299,7 @@ async function runVisualScenario(options2) {
|
|
|
1364
1299
|
const snapshots = captureEngine.getSnapshots();
|
|
1365
1300
|
const consoleErrors = consoleTracker.getErrors();
|
|
1366
1301
|
const consoleWarnings = consoleTracker.getWarnings();
|
|
1367
|
-
const
|
|
1302
|
+
const reportData = {
|
|
1368
1303
|
scenario,
|
|
1369
1304
|
snapshots,
|
|
1370
1305
|
consoleErrors,
|
|
@@ -1372,16 +1307,9 @@ async function runVisualScenario(options2) {
|
|
|
1372
1307
|
outputDir: scenarioArtifactsDir,
|
|
1373
1308
|
targetMode,
|
|
1374
1309
|
durationMs
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (import_fs6.default.existsSync(latestDir)) {
|
|
1379
|
-
import_fs6.default.rmSync(latestDir, { recursive: true, force: true });
|
|
1380
|
-
}
|
|
1381
|
-
import_fs6.default.mkdirSync(latestDir, { recursive: true });
|
|
1382
|
-
import_fs6.default.copyFileSync(reportPath, import_path6.default.join(latestDir, "report.md"));
|
|
1383
|
-
} catch {
|
|
1384
|
-
}
|
|
1310
|
+
};
|
|
1311
|
+
const reportPath = VisualReporter.generateReport(reportData);
|
|
1312
|
+
syncLatestArtifacts(artifactsRoot, scenarioArtifactsDir, reportPath, snapshots);
|
|
1385
1313
|
return {
|
|
1386
1314
|
scenarioId: scenario.id,
|
|
1387
1315
|
targetMode,
|
|
@@ -1394,6 +1322,7 @@ async function runVisualScenario(options2) {
|
|
|
1394
1322
|
error: err.message
|
|
1395
1323
|
};
|
|
1396
1324
|
} finally {
|
|
1325
|
+
await pluginManager.runTeardown(hookContext);
|
|
1397
1326
|
if (typeof scenario.teardown === "function") {
|
|
1398
1327
|
try {
|
|
1399
1328
|
console.log(`\u{1F9F9} [Scenario Teardown] Executing teardown hook...`);
|
|
@@ -1402,21 +1331,21 @@ async function runVisualScenario(options2) {
|
|
|
1402
1331
|
console.error(`\u26A0\uFE0F [Scenario Teardown] Error during teardown:`, teardownErr.message);
|
|
1403
1332
|
}
|
|
1404
1333
|
}
|
|
1405
|
-
if (
|
|
1406
|
-
for (const cleanTarget of
|
|
1334
|
+
if (options.cleanPaths && options.cleanPaths.length > 0) {
|
|
1335
|
+
for (const cleanTarget of options.cleanPaths) {
|
|
1407
1336
|
try {
|
|
1408
|
-
const resolvedCleanPath =
|
|
1409
|
-
if (
|
|
1337
|
+
const resolvedCleanPath = import_path7.default.resolve(process.cwd(), cleanTarget);
|
|
1338
|
+
if (import_fs7.default.existsSync(resolvedCleanPath)) {
|
|
1410
1339
|
console.log(`\u{1F9F9} [Auto-Cleanup] Removing: ${resolvedCleanPath}`);
|
|
1411
|
-
|
|
1340
|
+
import_fs7.default.rmSync(resolvedCleanPath, { recursive: true, force: true });
|
|
1412
1341
|
}
|
|
1413
1342
|
} catch (cleanErr) {
|
|
1414
1343
|
console.error(`\u26A0\uFE0F [Auto-Cleanup] Failed to remove ${cleanTarget}:`, cleanErr.message);
|
|
1415
1344
|
}
|
|
1416
1345
|
}
|
|
1417
1346
|
}
|
|
1418
|
-
if (!
|
|
1419
|
-
if (
|
|
1347
|
+
if (!options.detach) {
|
|
1348
|
+
if (customDriverStop) await customDriverStop();
|
|
1420
1349
|
if (previewDriver) await previewDriver.stop();
|
|
1421
1350
|
if (processManager) await processManager.stop();
|
|
1422
1351
|
} else {
|
|
@@ -1425,12 +1354,9 @@ async function runVisualScenario(options2) {
|
|
|
1425
1354
|
}
|
|
1426
1355
|
}
|
|
1427
1356
|
|
|
1428
|
-
// src/app/cli.ts
|
|
1429
|
-
var import_jiti = require("jiti");
|
|
1430
|
-
|
|
1431
1357
|
// src/features/snap/snap.ts
|
|
1432
|
-
var
|
|
1433
|
-
var
|
|
1358
|
+
var import_path8 = __toESM(require("path"));
|
|
1359
|
+
var import_fs8 = __toESM(require("fs"));
|
|
1434
1360
|
var PRESET_MAP = {
|
|
1435
1361
|
default: VIEWPORT_PRESETS.DEFAULT,
|
|
1436
1362
|
desktop: VIEWPORT_PRESETS.DEFAULT,
|
|
@@ -1477,18 +1403,18 @@ async function detectActiveDevServer() {
|
|
|
1477
1403
|
}
|
|
1478
1404
|
return null;
|
|
1479
1405
|
}
|
|
1480
|
-
async function runQuickSnap(
|
|
1481
|
-
let targetUrl =
|
|
1406
|
+
async function runQuickSnap(options) {
|
|
1407
|
+
let targetUrl = options.url;
|
|
1482
1408
|
let useStaticPreview = false;
|
|
1483
1409
|
let wwwrootDir;
|
|
1484
|
-
if (!targetUrl && !
|
|
1410
|
+
if (!targetUrl && !options.start) {
|
|
1485
1411
|
const activeUrl = await detectActiveDevServer();
|
|
1486
1412
|
if (activeUrl) {
|
|
1487
1413
|
targetUrl = activeUrl;
|
|
1488
1414
|
console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
|
|
1489
1415
|
} else {
|
|
1490
1416
|
const candidateDir = resolveWwwrootDir();
|
|
1491
|
-
if (
|
|
1417
|
+
if (import_fs8.default.existsSync(candidateDir) && import_fs8.default.existsSync(import_path8.default.join(candidateDir, "index.html"))) {
|
|
1492
1418
|
useStaticPreview = true;
|
|
1493
1419
|
wwwrootDir = candidateDir;
|
|
1494
1420
|
console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
|
|
@@ -1503,17 +1429,17 @@ async function runQuickSnap(options2) {
|
|
|
1503
1429
|
return false;
|
|
1504
1430
|
}
|
|
1505
1431
|
}
|
|
1506
|
-
} else if (!targetUrl &&
|
|
1432
|
+
} else if (!targetUrl && options.start) {
|
|
1507
1433
|
targetUrl = "http://localhost:5173";
|
|
1508
1434
|
}
|
|
1509
|
-
const waitMs =
|
|
1510
|
-
const snapshotPrefix =
|
|
1511
|
-
const targetViewports = parseViewportPresets(
|
|
1435
|
+
const waitMs = options.waitMs ?? 1e3;
|
|
1436
|
+
const snapshotPrefix = options.name || "quick_snap";
|
|
1437
|
+
const targetViewports = parseViewportPresets(options.viewports);
|
|
1512
1438
|
console.log(`
|
|
1513
1439
|
\u{1F4F8} [Quick Snap] Preparing instant verification for: ${targetUrl || wwwrootDir}`);
|
|
1514
1440
|
console.log(`\u{1F4D0} [Quick Snap] Testing ${targetViewports.length} viewports: ${targetViewports.map((v) => `${v.name} (${v.width}x${v.height})`).join(", ")}`);
|
|
1515
|
-
if (
|
|
1516
|
-
console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${
|
|
1441
|
+
if (options.selector) {
|
|
1442
|
+
console.log(`\u{1F3AF} [Quick Snap] Focused element selector: "${options.selector}"`);
|
|
1517
1443
|
}
|
|
1518
1444
|
const snapScenario = defineVisualTest({
|
|
1519
1445
|
id: "quick-snap",
|
|
@@ -1530,16 +1456,16 @@ async function runQuickSnap(options2) {
|
|
|
1530
1456
|
ctx.log(`Switching viewport to: ${vp.name} (${vp.width}x${vp.height})`);
|
|
1531
1457
|
await ctx.setPreset(vp);
|
|
1532
1458
|
await ctx.wait(200);
|
|
1533
|
-
await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}
|
|
1459
|
+
await ctx.capture(`${stepNum}_${snapshotPrefix}_${vp.name}`, { fullPage: options.fullPage });
|
|
1534
1460
|
}
|
|
1535
|
-
if (
|
|
1536
|
-
ctx.log(`Focusing on selector: "${
|
|
1461
|
+
if (options.selector) {
|
|
1462
|
+
ctx.log(`Focusing on selector: "${options.selector}"`);
|
|
1537
1463
|
try {
|
|
1538
1464
|
await ctx.setPreset(VIEWPORT_PRESETS.DEFAULT);
|
|
1539
|
-
await ctx.resizeToFit(
|
|
1540
|
-
await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector:
|
|
1465
|
+
await ctx.resizeToFit(options.selector, 15);
|
|
1466
|
+
await ctx.capture(`99_${snapshotPrefix}_element_focus`, { selector: options.selector });
|
|
1541
1467
|
} catch (err) {
|
|
1542
|
-
ctx.log(`\u26A0\uFE0F Could not isolate selector "${
|
|
1468
|
+
ctx.log(`\u26A0\uFE0F Could not isolate selector "${options.selector}": ${err.message}`);
|
|
1543
1469
|
}
|
|
1544
1470
|
}
|
|
1545
1471
|
const errors = ctx.getConsoleErrors();
|
|
@@ -1552,18 +1478,19 @@ async function runQuickSnap(options2) {
|
|
|
1552
1478
|
});
|
|
1553
1479
|
const result = await runVisualScenario({
|
|
1554
1480
|
scenario: snapScenario,
|
|
1555
|
-
targetMode:
|
|
1481
|
+
targetMode: options.mode || "preview",
|
|
1556
1482
|
url: useStaticPreview ? void 0 : targetUrl,
|
|
1557
1483
|
wwwrootDir: useStaticPreview ? wwwrootDir : void 0,
|
|
1558
|
-
startCommand:
|
|
1559
|
-
startCwd:
|
|
1560
|
-
executablePath:
|
|
1561
|
-
cleanPaths:
|
|
1562
|
-
cleanArtifacts:
|
|
1563
|
-
port:
|
|
1564
|
-
headed:
|
|
1565
|
-
detach:
|
|
1566
|
-
artifactsRoot:
|
|
1484
|
+
startCommand: options.start,
|
|
1485
|
+
startCwd: options.startCwd,
|
|
1486
|
+
executablePath: options.exe,
|
|
1487
|
+
cleanPaths: options.clean,
|
|
1488
|
+
cleanArtifacts: options.cleanArtifacts,
|
|
1489
|
+
port: options.port,
|
|
1490
|
+
headed: options.headed,
|
|
1491
|
+
detach: options.detach,
|
|
1492
|
+
artifactsRoot: options.outDir ? import_path8.default.resolve(process.cwd(), options.outDir) : void 0,
|
|
1493
|
+
plugins: options.plugins
|
|
1567
1494
|
});
|
|
1568
1495
|
console.log(`
|
|
1569
1496
|
========================================`);
|
|
@@ -1577,100 +1504,112 @@ async function runQuickSnap(options2) {
|
|
|
1577
1504
|
return result.success && result.consoleErrors === 0;
|
|
1578
1505
|
}
|
|
1579
1506
|
|
|
1580
|
-
// src/app/
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
-
|
|
1507
|
+
// src/app/lib/argsParser.ts
|
|
1508
|
+
function parseCliArgs(argv, fileConfig) {
|
|
1509
|
+
const isInitCommand = argv[0] === "init";
|
|
1510
|
+
const isSnapCommand = argv[0] === "snap";
|
|
1511
|
+
const effectiveArgs = isSnapCommand ? argv.slice(1) : argv;
|
|
1512
|
+
const options = {
|
|
1513
|
+
mode: fileConfig.mode || "preview",
|
|
1514
|
+
port: fileConfig.port || 9222,
|
|
1515
|
+
headed: fileConfig.headed ?? false,
|
|
1516
|
+
detach: fileConfig.detach ?? false,
|
|
1517
|
+
build: fileConfig.buildCommand || false,
|
|
1518
|
+
start: fileConfig.startCommand,
|
|
1519
|
+
startCwd: fileConfig.startCwd,
|
|
1520
|
+
cleanArtifacts: fileConfig.cleanArtifacts ?? false,
|
|
1521
|
+
url: fileConfig.url,
|
|
1522
|
+
exe: fileConfig.executablePath,
|
|
1523
|
+
clean: Array.isArray(fileConfig.clean) ? fileConfig.clean : fileConfig.clean ? [fileConfig.clean] : void 0,
|
|
1524
|
+
dir: fileConfig.scenarios,
|
|
1525
|
+
wwwroot: fileConfig.wwwroot,
|
|
1526
|
+
outDir: fileConfig.outDir,
|
|
1527
|
+
plugins: fileConfig.plugins ? [...fileConfig.plugins] : []
|
|
1528
|
+
};
|
|
1529
|
+
for (const arg of effectiveArgs) {
|
|
1530
|
+
if (arg === "--help" || arg === "-h") {
|
|
1531
|
+
options.help = true;
|
|
1532
|
+
} else if (arg.startsWith("--scenario=")) {
|
|
1533
|
+
options.scenario = arg.split("=")[1];
|
|
1534
|
+
} else if (arg === "--all") {
|
|
1535
|
+
options.all = true;
|
|
1536
|
+
} else if (arg.startsWith("--mode=")) {
|
|
1537
|
+
options.mode = arg.split("=")[1].toLowerCase();
|
|
1538
|
+
} else if (arg.startsWith("--port=")) {
|
|
1539
|
+
options.port = parseInt(arg.split("=")[1], 10) || 9222;
|
|
1540
|
+
} else if (arg === "--headed") {
|
|
1541
|
+
options.headed = true;
|
|
1542
|
+
} else if (arg === "--detach") {
|
|
1543
|
+
options.detach = true;
|
|
1544
|
+
} else if (arg === "--build") {
|
|
1545
|
+
options.build = true;
|
|
1546
|
+
} else if (arg.startsWith("--build=")) {
|
|
1547
|
+
options.build = arg.slice("--build=".length);
|
|
1548
|
+
} else if (arg.startsWith("--start=")) {
|
|
1549
|
+
options.start = arg.slice("--start=".length);
|
|
1550
|
+
} else if (arg.startsWith("--start-cwd=") || arg.startsWith("--cwd=")) {
|
|
1551
|
+
options.startCwd = arg.split("=")[1];
|
|
1552
|
+
} else if (arg === "--clean-artifacts") {
|
|
1553
|
+
options.cleanArtifacts = true;
|
|
1554
|
+
} else if (arg.startsWith("--url=")) {
|
|
1555
|
+
options.url = arg.split("=")[1];
|
|
1556
|
+
} else if (arg.startsWith("--selector=")) {
|
|
1557
|
+
options.selector = arg.split("=")[1];
|
|
1558
|
+
} else if (arg.startsWith("--viewports=")) {
|
|
1559
|
+
const raw = arg.slice("--viewports=".length);
|
|
1560
|
+
options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1561
|
+
} else if (arg.startsWith("--wait=")) {
|
|
1562
|
+
options.waitMs = parseInt(arg.slice("--wait=".length), 10);
|
|
1563
|
+
} else if (arg.startsWith("--name=")) {
|
|
1564
|
+
options.name = arg.slice("--name=".length);
|
|
1565
|
+
} else if (arg.startsWith("--exe=") || arg.startsWith("--executable=")) {
|
|
1566
|
+
const prefix = arg.startsWith("--exe=") ? "--exe=" : "--executable=";
|
|
1567
|
+
options.exe = arg.slice(prefix.length);
|
|
1568
|
+
} else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
|
|
1569
|
+
const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
|
|
1570
|
+
const rawPaths = arg.slice(prefix.length);
|
|
1571
|
+
options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1572
|
+
} else if (arg.startsWith("--dir=")) {
|
|
1573
|
+
options.dir = arg.split("=")[1];
|
|
1574
|
+
} else if (arg.startsWith("--wwwroot=")) {
|
|
1575
|
+
options.wwwroot = arg.split("=")[1];
|
|
1576
|
+
} else if (arg.startsWith("--outDir=") || arg.startsWith("--folder=")) {
|
|
1577
|
+
options.outDir = arg.split("=")[1];
|
|
1578
|
+
} else if (arg.startsWith("--plugin=") || arg.startsWith("--plugins=")) {
|
|
1579
|
+
const raw = arg.split("=")[1] || "";
|
|
1580
|
+
const items = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1581
|
+
options.plugins = [...options.plugins || [], ...items];
|
|
1582
|
+
} else if (arg === "--full" || arg === "--full-page") {
|
|
1583
|
+
options.fullPage = true;
|
|
1584
|
+
}
|
|
1658
1585
|
}
|
|
1586
|
+
if (options.start && !options.startCwd) {
|
|
1587
|
+
options.startCwd = detectStartCwd(options.startCwd);
|
|
1588
|
+
}
|
|
1589
|
+
if ((options.mode === "desktop" || options.exe) && !options.plugins?.includes("desktop-webview2")) {
|
|
1590
|
+
options.plugins = ["desktop-webview2", ...options.plugins || []];
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
isSnapCommand,
|
|
1594
|
+
isInitCommand,
|
|
1595
|
+
options
|
|
1596
|
+
};
|
|
1659
1597
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1598
|
+
|
|
1599
|
+
// src/app/lib/help.ts
|
|
1663
1600
|
function printHelp() {
|
|
1664
1601
|
console.log(`
|
|
1665
1602
|
\u{1F441}\uFE0F AgentLens - Visual UI Self-Verification for AI Agents
|
|
1666
1603
|
|
|
1667
1604
|
Usage:
|
|
1668
1605
|
npx agent-lens snap [options] Instant one-shot visual & console check (no test files needed)
|
|
1606
|
+
npx agent-lens live [action] Interactive live session (start, click, type, snap, stop)
|
|
1669
1607
|
npx agent-lens [options] Run scripted scenario tests from scenarios/
|
|
1670
1608
|
npx agent-lens init Generate starter scenario template & mocks
|
|
1671
1609
|
|
|
1672
1610
|
Commands:
|
|
1673
1611
|
snap Take immediate multi-viewport screenshots of a URL & check console errors
|
|
1612
|
+
live Interactive control: click coordinates/selectors, type text, snap --full
|
|
1674
1613
|
init Generate starter template in scenarios/template.scenario.ts and mocks.ts
|
|
1675
1614
|
|
|
1676
1615
|
Options:
|
|
@@ -1678,6 +1617,7 @@ Options:
|
|
|
1678
1617
|
--start="<cmd>" Launch dev server or backend process before testing (e.g. --start="npm run dev")
|
|
1679
1618
|
--start-cwd=<path> Directory to execute --start command in (e.g. --start-cwd=./Frontend)
|
|
1680
1619
|
--clean-artifacts Purge previous test artifacts to prevent folder bloat
|
|
1620
|
+
--full Capture full scrollable page instead of only the viewport
|
|
1681
1621
|
--selector=<css> Target a specific element to focus on / resize-to-fit
|
|
1682
1622
|
--viewports=<list> Comma-separated viewport presets (default: desktop,mobile; or 1200x800,375x667)
|
|
1683
1623
|
--wait=<ms> Wait time in milliseconds after loading before snapshotting [default: 1000]
|
|
@@ -1689,6 +1629,7 @@ Options:
|
|
|
1689
1629
|
--port=<port> CDP remote debugging port [default: 9222]
|
|
1690
1630
|
--build[=<cmd>] Run build command before testing (e.g. --build="dotnet build" or npm run build)
|
|
1691
1631
|
--clean=<paths> Comma-separated paths to safely delete upon test exit (e.g. --clean="./temp,./cache")
|
|
1632
|
+
--plugin=<names> Comma-separated plugins (e.g. --plugin=desktop-webview2,mock-ipc)
|
|
1692
1633
|
--headed Show Chromium browser window
|
|
1693
1634
|
--detach Keep browser/app open after finishing
|
|
1694
1635
|
--dir=<path> Custom scenarios directory [default: scenarios]
|
|
@@ -1697,13 +1638,17 @@ Options:
|
|
|
1697
1638
|
--help, -h Show this help message
|
|
1698
1639
|
`);
|
|
1699
1640
|
}
|
|
1641
|
+
|
|
1642
|
+
// src/app/lib/templateInit.ts
|
|
1643
|
+
var import_path9 = __toESM(require("path"));
|
|
1644
|
+
var import_fs9 = __toESM(require("fs"));
|
|
1700
1645
|
function initScenarioTemplate() {
|
|
1701
|
-
const targetDir =
|
|
1702
|
-
if (!
|
|
1703
|
-
|
|
1646
|
+
const targetDir = import_path9.default.resolve(process.cwd(), "scenarios");
|
|
1647
|
+
if (!import_fs9.default.existsSync(targetDir)) {
|
|
1648
|
+
import_fs9.default.mkdirSync(targetDir, { recursive: true });
|
|
1704
1649
|
}
|
|
1705
|
-
const templatePath =
|
|
1706
|
-
if (!
|
|
1650
|
+
const templatePath = import_path9.default.join(targetDir, "template.scenario.ts");
|
|
1651
|
+
if (!import_fs9.default.existsSync(templatePath)) {
|
|
1707
1652
|
const templateContent = `import { defineVisualTest, VIEWPORT_PRESETS } from 'agent-lens';
|
|
1708
1653
|
|
|
1709
1654
|
export default defineVisualTest({
|
|
@@ -1734,13 +1679,13 @@ export default defineVisualTest({
|
|
|
1734
1679
|
}
|
|
1735
1680
|
});
|
|
1736
1681
|
`;
|
|
1737
|
-
|
|
1682
|
+
import_fs9.default.writeFileSync(templatePath, templateContent, "utf8");
|
|
1738
1683
|
console.log(`\u2705 Starter scenario generated at: ${templatePath}`);
|
|
1739
1684
|
} else {
|
|
1740
1685
|
console.log(`\u2139\uFE0F Template already exists at: ${templatePath}`);
|
|
1741
1686
|
}
|
|
1742
|
-
const mocksPath =
|
|
1743
|
-
if (!
|
|
1687
|
+
const mocksPath = import_path9.default.join(targetDir, "mocks.ts");
|
|
1688
|
+
if (!import_fs9.default.existsSync(mocksPath)) {
|
|
1744
1689
|
const mocksContent = `/**
|
|
1745
1690
|
* Global IPC & API Mocks
|
|
1746
1691
|
*
|
|
@@ -1751,21 +1696,21 @@ export default [
|
|
|
1751
1696
|
{ action: 'GET_USER_PROFILE', data: { id: 1, name: 'Agent', role: 'admin' } }
|
|
1752
1697
|
];
|
|
1753
1698
|
`;
|
|
1754
|
-
|
|
1699
|
+
import_fs9.default.writeFileSync(mocksPath, mocksContent, "utf8");
|
|
1755
1700
|
console.log(`\u2705 Base global mocks generated at: ${mocksPath}`);
|
|
1756
1701
|
}
|
|
1757
1702
|
console.log(`\u{1F449} Run tests with: npx agent-lens --scenario=template --mode=preview`);
|
|
1758
1703
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1704
|
+
|
|
1705
|
+
// src/app/lib/scenarioFinder.ts
|
|
1706
|
+
var import_path10 = __toESM(require("path"));
|
|
1707
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1763
1708
|
async function findScenarios(dir, specificName) {
|
|
1764
1709
|
const results = [];
|
|
1765
|
-
if (!
|
|
1766
|
-
const items =
|
|
1710
|
+
if (!import_fs10.default.existsSync(dir)) return results;
|
|
1711
|
+
const items = import_fs10.default.readdirSync(dir, { withFileTypes: true });
|
|
1767
1712
|
for (const item of items) {
|
|
1768
|
-
const fullPath =
|
|
1713
|
+
const fullPath = import_path10.default.join(dir, item.name);
|
|
1769
1714
|
if (item.isDirectory()) {
|
|
1770
1715
|
results.push(...await findScenarios(fullPath, specificName));
|
|
1771
1716
|
} else if (item.name.endsWith(".scenario.ts") || item.name.endsWith(".scenario.js")) {
|
|
@@ -1778,7 +1723,7 @@ async function findScenarios(dir, specificName) {
|
|
|
1778
1723
|
}
|
|
1779
1724
|
function resolveScenariosDirectory(customDir) {
|
|
1780
1725
|
if (customDir) {
|
|
1781
|
-
return
|
|
1726
|
+
return import_path10.default.resolve(process.cwd(), customDir);
|
|
1782
1727
|
}
|
|
1783
1728
|
const candidates = [
|
|
1784
1729
|
"scenarios",
|
|
@@ -1788,121 +1733,333 @@ function resolveScenariosDirectory(customDir) {
|
|
|
1788
1733
|
"src/scenarios"
|
|
1789
1734
|
];
|
|
1790
1735
|
for (const c of candidates) {
|
|
1791
|
-
const p =
|
|
1792
|
-
if (
|
|
1736
|
+
const p = import_path10.default.resolve(process.cwd(), c);
|
|
1737
|
+
if (import_fs10.default.existsSync(p)) {
|
|
1793
1738
|
return p;
|
|
1794
1739
|
}
|
|
1795
1740
|
}
|
|
1796
|
-
return
|
|
1741
|
+
return import_path10.default.resolve(process.cwd(), "scenarios");
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/plugins/live-controller/actions.ts
|
|
1745
|
+
var import_path12 = __toESM(require("path"));
|
|
1746
|
+
|
|
1747
|
+
// src/plugins/live-controller/session.ts
|
|
1748
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1749
|
+
var import_path11 = __toESM(require("path"));
|
|
1750
|
+
var import_tree_kill2 = __toESM(require("tree-kill"));
|
|
1751
|
+
var SESSION_DIR = import_path11.default.resolve(process.cwd(), ".agent-lens");
|
|
1752
|
+
var SESSION_FILE = import_path11.default.join(SESSION_DIR, "live-session.json");
|
|
1753
|
+
var LIVE_ARTIFACTS_DIR = import_path11.default.resolve(process.cwd(), "artifacts", "live");
|
|
1754
|
+
function getLiveArtifactsDir() {
|
|
1755
|
+
if (!import_fs11.default.existsSync(LIVE_ARTIFACTS_DIR)) {
|
|
1756
|
+
import_fs11.default.mkdirSync(LIVE_ARTIFACTS_DIR, { recursive: true });
|
|
1757
|
+
}
|
|
1758
|
+
return LIVE_ARTIFACTS_DIR;
|
|
1759
|
+
}
|
|
1760
|
+
function readLiveSession() {
|
|
1761
|
+
if (!import_fs11.default.existsSync(SESSION_FILE)) return null;
|
|
1762
|
+
try {
|
|
1763
|
+
return JSON.parse(import_fs11.default.readFileSync(SESSION_FILE, "utf-8"));
|
|
1764
|
+
} catch {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function saveLiveSession(info) {
|
|
1769
|
+
if (!import_fs11.default.existsSync(SESSION_DIR)) {
|
|
1770
|
+
import_fs11.default.mkdirSync(SESSION_DIR, { recursive: true });
|
|
1771
|
+
}
|
|
1772
|
+
import_fs11.default.writeFileSync(SESSION_FILE, JSON.stringify(info, null, 2), "utf-8");
|
|
1773
|
+
}
|
|
1774
|
+
function clearLiveSession() {
|
|
1775
|
+
if (import_fs11.default.existsSync(SESSION_FILE)) {
|
|
1776
|
+
try {
|
|
1777
|
+
import_fs11.default.unlinkSync(SESSION_FILE);
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1797
1781
|
}
|
|
1798
|
-
async function
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1782
|
+
async function connectToLiveSession() {
|
|
1783
|
+
const session = readLiveSession();
|
|
1784
|
+
if (!session) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
`No active live session found. Start one with: npx agent-lens live start --url=http://localhost:5173`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${session.port}`);
|
|
1790
|
+
const contexts = browser.contexts();
|
|
1791
|
+
const context = contexts[0] || await browser.newContext();
|
|
1792
|
+
const pages = context.pages();
|
|
1793
|
+
const page = pages[0] || await context.waitForEvent("page", { timeout: 5e3 });
|
|
1794
|
+
return { browser, context, page };
|
|
1795
|
+
}
|
|
1796
|
+
async function stopLiveSession() {
|
|
1797
|
+
const session = readLiveSession();
|
|
1798
|
+
if (!session) {
|
|
1799
|
+
console.log(`\u2139\uFE0F [Live] No active session to stop.`);
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
const { browser } = await connectToLiveSession();
|
|
1804
|
+
await browser.close().catch(() => {
|
|
1816
1805
|
});
|
|
1817
|
-
|
|
1806
|
+
} catch {
|
|
1818
1807
|
}
|
|
1819
|
-
if (
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1808
|
+
if (session.pid) {
|
|
1809
|
+
await new Promise((resolve) => {
|
|
1810
|
+
(0, import_tree_kill2.default)(session.pid, "SIGTERM", () => resolve());
|
|
1811
|
+
});
|
|
1823
1812
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1813
|
+
clearLiveSession();
|
|
1814
|
+
console.log(`\u{1F6D1} [Live] Session stopped and cleaned up.`);
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// src/plugins/live-controller/actions.ts
|
|
1819
|
+
async function clickCoords(page, x, y, options) {
|
|
1820
|
+
console.log(`\u{1F5B1}\uFE0F [LiveController] Clicking coordinates: (${x}, ${y})`);
|
|
1821
|
+
await page.mouse.click(x, y, options);
|
|
1822
|
+
await page.waitForTimeout(200);
|
|
1823
|
+
}
|
|
1824
|
+
async function snapLive(page, options) {
|
|
1825
|
+
const artifactsDir = getLiveArtifactsDir();
|
|
1826
|
+
const fileName = options?.name ? `${options.name}.png` : "current.png";
|
|
1827
|
+
const filePath = import_path12.default.join(artifactsDir, fileName);
|
|
1828
|
+
if (options?.selector) {
|
|
1829
|
+
const el = await page.waitForSelector(options.selector, { timeout: 5e3 });
|
|
1830
|
+
await el.screenshot({ path: filePath });
|
|
1831
|
+
} else {
|
|
1832
|
+
await page.screenshot({ path: filePath, fullPage: options?.fullPage ?? false });
|
|
1830
1833
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1834
|
+
console.log(`\u{1F4F8} [LiveController] Snapshot saved: ${filePath}${options?.fullPage ? " (Full Page)" : ""}`);
|
|
1835
|
+
return filePath;
|
|
1836
|
+
}
|
|
1837
|
+
async function handleLiveCli(argv) {
|
|
1838
|
+
const action = argv[0]?.toLowerCase();
|
|
1839
|
+
switch (action) {
|
|
1840
|
+
case "start": {
|
|
1841
|
+
const urlArg = argv.find((a) => a.startsWith("--url="))?.split("=")[1] || "http://localhost:5173";
|
|
1842
|
+
const portArg = parseInt(argv.find((a) => a.startsWith("--port="))?.split("=")[1] || "9223", 10);
|
|
1843
|
+
const isHeaded = argv.includes("--headed");
|
|
1844
|
+
console.log(`\u{1F680} [Live] Starting background browser for ${urlArg} on CDP port ${portArg}...`);
|
|
1845
|
+
const browser = await chromium.launch({
|
|
1846
|
+
headless: !isHeaded,
|
|
1847
|
+
args: [`--remote-debugging-port=${portArg}`, "--no-sandbox"]
|
|
1848
|
+
});
|
|
1849
|
+
const context = await browser.newContext({ viewport: { width: 1200, height: 800 } });
|
|
1850
|
+
const page = await context.newPage();
|
|
1851
|
+
await page.goto(urlArg, { waitUntil: "domcontentloaded" });
|
|
1852
|
+
await page.waitForTimeout(500);
|
|
1853
|
+
saveLiveSession({
|
|
1854
|
+
port: portArg,
|
|
1855
|
+
url: urlArg,
|
|
1856
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1857
|
+
});
|
|
1858
|
+
await snapLive(page, { name: "current" });
|
|
1859
|
+
console.log(`\u2705 [Live] Session active! You can now send live commands:
|
|
1860
|
+
`);
|
|
1861
|
+
console.log(` npx agent-lens live click 450 120`);
|
|
1862
|
+
console.log(` npx agent-lens live type "input" "hello"`);
|
|
1863
|
+
console.log(` npx agent-lens live snap --full`);
|
|
1864
|
+
console.log(` npx agent-lens live stop
|
|
1865
|
+
`);
|
|
1866
|
+
return true;
|
|
1841
1867
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
const m = await jiti.import(mocksPath);
|
|
1858
|
-
globalMocks = m.default || m.mocks || [];
|
|
1859
|
-
console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path8.default.basename(mocksPath)}`);
|
|
1860
|
-
break;
|
|
1861
|
-
} catch (err) {
|
|
1862
|
-
console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
|
|
1868
|
+
case "click": {
|
|
1869
|
+
const { page, browser } = await connectToLiveSession();
|
|
1870
|
+
const firstArg = argv[1];
|
|
1871
|
+
const secondArg = argv[2];
|
|
1872
|
+
const x = parseInt(firstArg, 10);
|
|
1873
|
+
const y = parseInt(secondArg, 10);
|
|
1874
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
1875
|
+
await clickCoords(page, x, y);
|
|
1876
|
+
} else if (firstArg) {
|
|
1877
|
+
console.log(`\u{1F5B1}\uFE0F [Live] Clicking selector: "${firstArg}"`);
|
|
1878
|
+
await page.click(firstArg);
|
|
1879
|
+
} else {
|
|
1880
|
+
console.error(`\u274C Usage: npx agent-lens live click <x> <y> OR npx agent-lens live click <selector>`);
|
|
1881
|
+
await browser.close();
|
|
1882
|
+
return false;
|
|
1863
1883
|
}
|
|
1884
|
+
await snapLive(page, { name: "current" });
|
|
1885
|
+
return true;
|
|
1864
1886
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
continue;
|
|
1887
|
+
case "type": {
|
|
1888
|
+
const { page, browser } = await connectToLiveSession();
|
|
1889
|
+
const selector = argv[1];
|
|
1890
|
+
const text = argv[2];
|
|
1891
|
+
if (!selector || text === void 0) {
|
|
1892
|
+
console.error(`\u274C Usage: npx agent-lens live type <selector> <text>`);
|
|
1893
|
+
await browser.close();
|
|
1894
|
+
return false;
|
|
1874
1895
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1896
|
+
console.log(`\u2328\uFE0F [Live] Typing into "${selector}": "${text}"`);
|
|
1897
|
+
await page.fill(selector, text);
|
|
1898
|
+
await snapLive(page, { name: "current" });
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
case "snap": {
|
|
1902
|
+
const { page } = await connectToLiveSession();
|
|
1903
|
+
const isFull = argv.includes("--full");
|
|
1904
|
+
const nameArg = argv.find((a) => !a.startsWith("--") && a !== "snap");
|
|
1905
|
+
await snapLive(page, { name: nameArg || "current", fullPage: isFull });
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
case "stop": {
|
|
1909
|
+
return await stopLiveSession();
|
|
1910
|
+
}
|
|
1911
|
+
default: {
|
|
1912
|
+
console.log(`
|
|
1913
|
+
\u2139\uFE0F AgentLens Live Controller:
|
|
1914
|
+
npx agent-lens live start --url=<url> Start background live session
|
|
1915
|
+
npx agent-lens live click <x> <y> Click at pixel coordinates
|
|
1916
|
+
npx agent-lens live click <selector> Click CSS selector
|
|
1917
|
+
npx agent-lens live type <sel> <text> Fill input field
|
|
1918
|
+
npx agent-lens live snap [name] [--full] Capture current or full-page screenshot
|
|
1919
|
+
npx agent-lens live stop Close live browser and finish
|
|
1920
|
+
`);
|
|
1921
|
+
return true;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/app/cli.ts
|
|
1927
|
+
var rawArgs = process.argv.slice(2);
|
|
1928
|
+
if (rawArgs[0] === "live") {
|
|
1929
|
+
handleLiveCli(rawArgs.slice(1)).then((success) => process.exit(success ? 0 : 1)).catch((err) => {
|
|
1930
|
+
console.error("\u274C Live command failed:", err instanceof Error ? err.message : err);
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
});
|
|
1933
|
+
} else {
|
|
1934
|
+
const fileConfig = loadConfig();
|
|
1935
|
+
const { isSnapCommand, isInitCommand, options } = parseCliArgs(rawArgs, fileConfig);
|
|
1936
|
+
if (isInitCommand) {
|
|
1937
|
+
initScenarioTemplate();
|
|
1938
|
+
process.exit(0);
|
|
1939
|
+
}
|
|
1940
|
+
if (options.help) {
|
|
1941
|
+
printHelp();
|
|
1942
|
+
process.exit(0);
|
|
1943
|
+
}
|
|
1944
|
+
async function main() {
|
|
1945
|
+
if (isSnapCommand || options.url && !options.scenario && !options.all) {
|
|
1946
|
+
const snapSuccess = await runQuickSnap({
|
|
1878
1947
|
url: options.url,
|
|
1879
|
-
|
|
1948
|
+
start: options.start,
|
|
1880
1949
|
startCwd: options.startCwd,
|
|
1950
|
+
selector: options.selector,
|
|
1951
|
+
viewports: options.viewports,
|
|
1952
|
+
waitMs: options.waitMs,
|
|
1953
|
+
name: options.name,
|
|
1954
|
+
clean: options.clean,
|
|
1881
1955
|
cleanArtifacts: options.cleanArtifacts,
|
|
1882
|
-
port: options.port,
|
|
1883
1956
|
headed: options.headed,
|
|
1884
1957
|
detach: options.detach,
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1958
|
+
outDir: options.outDir,
|
|
1959
|
+
mode: options.mode,
|
|
1960
|
+
exe: options.exe,
|
|
1961
|
+
port: options.port,
|
|
1962
|
+
plugins: options.plugins,
|
|
1963
|
+
fullPage: options.fullPage
|
|
1891
1964
|
});
|
|
1892
|
-
|
|
1965
|
+
process.exit(snapSuccess ? 0 : 1);
|
|
1966
|
+
}
|
|
1967
|
+
if (!options.scenario && !options.all) {
|
|
1968
|
+
console.error("\u274C Please specify a scenario (--scenario=name), run --all, or use: npx agent-lens snap --url=http://localhost:5173");
|
|
1969
|
+
printHelp();
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
const scenariosDir = resolveScenariosDirectory(options.dir);
|
|
1973
|
+
const scenarioPaths = await findScenarios(scenariosDir, options.scenario);
|
|
1974
|
+
if (scenarioPaths.length === 0) {
|
|
1975
|
+
console.error(`\u274C No scenarios found in ${scenariosDir}`);
|
|
1976
|
+
console.log(`\u{1F4A1} Generate a template with: npx agent-lens init`);
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
const shouldBuild = options.build || fileConfig.autoBuild;
|
|
1980
|
+
if (shouldBuild) {
|
|
1981
|
+
const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
|
|
1982
|
+
console.log(`
|
|
1983
|
+
\u{1F528} [Build] Running build process: "${buildCmd}"...`);
|
|
1984
|
+
try {
|
|
1985
|
+
(0, import_child_process2.execSync)(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
|
|
1986
|
+
} catch (e) {
|
|
1987
|
+
console.error(`\u274C Build failed: ${buildCmd}`);
|
|
1988
|
+
process.exit(1);
|
|
1989
|
+
}
|
|
1990
|
+
} else if (options.mode === "preview" && !options.url && !options.start) {
|
|
1991
|
+
console.log(`\u26A0\uFE0F Warning: Running without --build or --url flag. Make sure your frontend is built!`);
|
|
1992
|
+
}
|
|
1993
|
+
console.log(`\u{1F4CB} Found ${scenarioPaths.length} scenario(s) in: ${scenariosDir}`);
|
|
1994
|
+
const jiti = (0, import_jiti2.createJiti)(process.cwd());
|
|
1995
|
+
let globalMocks = [];
|
|
1996
|
+
const mockCandidates = [
|
|
1997
|
+
import_path13.default.join(scenariosDir, "mocks.ts"),
|
|
1998
|
+
import_path13.default.join(scenariosDir, "mocks.js"),
|
|
1999
|
+
import_path13.default.join(process.cwd(), "mocks.ts"),
|
|
2000
|
+
import_path13.default.join(process.cwd(), "mocks.js")
|
|
2001
|
+
];
|
|
2002
|
+
for (const mocksPath of mockCandidates) {
|
|
2003
|
+
if (import_fs12.default.existsSync(mocksPath)) {
|
|
2004
|
+
try {
|
|
2005
|
+
const m = await jiti.import(mocksPath);
|
|
2006
|
+
globalMocks = m.default || m.mocks || [];
|
|
2007
|
+
console.log(`\u{1F30D} Loaded ${globalMocks.length} global mock(s) from ${import_path13.default.basename(mocksPath)}`);
|
|
2008
|
+
break;
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
console.warn(`\u26A0\uFE0F Failed to load global mocks from ${mocksPath}:`, err);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (globalMocks.length > 0 && !options.plugins?.includes("mock-ipc")) {
|
|
2015
|
+
options.plugins = ["mock-ipc", ...options.plugins || []];
|
|
2016
|
+
}
|
|
2017
|
+
let allSuccess = true;
|
|
2018
|
+
for (const scenarioPath of scenarioPaths) {
|
|
2019
|
+
try {
|
|
2020
|
+
const scenarioModule = await jiti.import(scenarioPath);
|
|
2021
|
+
const scenario = scenarioModule.default || scenarioModule.scenario;
|
|
2022
|
+
if (!scenario || typeof scenario.run !== "function") {
|
|
2023
|
+
console.error(`\u26A0\uFE0F Skipped: file ${import_path13.default.basename(scenarioPath)} does not export a VisualScenario object by default.`);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
const scenarioPlugins = [...options.plugins || []];
|
|
2027
|
+
if (scenario.mockIpc && scenario.mockIpc.length > 0 && !scenarioPlugins.includes("mock-ipc")) {
|
|
2028
|
+
scenarioPlugins.push("mock-ipc");
|
|
2029
|
+
}
|
|
2030
|
+
const result = await runVisualScenario({
|
|
2031
|
+
scenario,
|
|
2032
|
+
targetMode: options.mode,
|
|
2033
|
+
url: options.url,
|
|
2034
|
+
startCommand: options.start,
|
|
2035
|
+
startCwd: options.startCwd,
|
|
2036
|
+
cleanArtifacts: options.cleanArtifacts,
|
|
2037
|
+
port: options.port,
|
|
2038
|
+
headed: options.headed,
|
|
2039
|
+
detach: options.detach,
|
|
2040
|
+
executablePath: options.exe,
|
|
2041
|
+
cleanPaths: options.clean,
|
|
2042
|
+
desktopEnv: fileConfig.env,
|
|
2043
|
+
wwwrootDir: options.wwwroot ? resolveWwwrootDir(options.wwwroot) : void 0,
|
|
2044
|
+
artifactsRoot: options.outDir ? import_path13.default.resolve(process.cwd(), options.outDir) : void 0,
|
|
2045
|
+
globalMocks,
|
|
2046
|
+
plugins: scenarioPlugins
|
|
2047
|
+
});
|
|
2048
|
+
if (!result.success) {
|
|
2049
|
+
allSuccess = false;
|
|
2050
|
+
}
|
|
2051
|
+
} catch (err) {
|
|
2052
|
+
console.error(`\u274C Failed to load scenario ${import_path13.default.basename(scenarioPath)}:`, err);
|
|
1893
2053
|
allSuccess = false;
|
|
1894
2054
|
}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
|
|
2055
|
+
}
|
|
2056
|
+
if (!allSuccess) {
|
|
2057
|
+
process.exit(1);
|
|
1898
2058
|
}
|
|
1899
2059
|
}
|
|
1900
|
-
|
|
2060
|
+
main().catch((err) => {
|
|
2061
|
+
console.error("Fatal error:", err);
|
|
1901
2062
|
process.exit(1);
|
|
1902
|
-
}
|
|
2063
|
+
});
|
|
1903
2064
|
}
|
|
1904
|
-
main().catch((err) => {
|
|
1905
|
-
console.error("Fatal error:", err);
|
|
1906
|
-
process.exit(1);
|
|
1907
|
-
});
|
|
1908
2065
|
//# sourceMappingURL=cli.js.map
|