@_deep4wee/agent-lens 1.0.1 → 1.1.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 +227 -221
- package/dist/cli.d.mts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +255 -200
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +255 -205
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +32 -9
- package/dist/index.d.ts +32 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/skills/agent-lens/SKILL.md +24 -6
- package/skills/agent-lens/examples/06-state-testing-with-mock-ipc.md +13 -12
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -26,6 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
25
|
// src/app/cli.ts
|
|
27
26
|
var import_path8 = __toESM(require("path"));
|
|
28
27
|
var import_fs8 = __toESM(require("fs"));
|
|
28
|
+
var import_child_process3 = require("child_process");
|
|
29
29
|
|
|
30
30
|
// src/features/runner/runner.ts
|
|
31
31
|
var import_path6 = __toESM(require("path"));
|
|
@@ -182,9 +182,9 @@ var VisualReporter = class {
|
|
|
182
182
|
burstGroups.set(s.burstGroup, list);
|
|
183
183
|
});
|
|
184
184
|
for (const snap of regularSnapshots) {
|
|
185
|
-
const
|
|
185
|
+
const relativeLink = snap.relativeUri || `./${snap.fileName}`;
|
|
186
186
|
rows.push(
|
|
187
|
-
`| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${
|
|
187
|
+
`| **${String(snap.index).padStart(2, "0")}** | \`${snap.viewport.width}x${snap.viewport.height}\` | ${snap.name} | [${snap.fileName}](${relativeLink}) |`
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
let burstSections = "";
|
|
@@ -201,8 +201,8 @@ var VisualReporter = class {
|
|
|
201
201
|
| :--- | :--- | :--- | :--- |
|
|
202
202
|
`;
|
|
203
203
|
for (const frame of frames) {
|
|
204
|
-
const
|
|
205
|
-
burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${
|
|
204
|
+
const relativeLink = frame.relativeUri || `./${frame.fileName}`;
|
|
205
|
+
burstSections += `| Frame ${frame.frameIndex} | \`${frame.viewport.width}x${frame.viewport.height}\` | [${frame.fileName}](${relativeLink}) |  |
|
|
206
206
|
`;
|
|
207
207
|
}
|
|
208
208
|
burstSections += `
|
|
@@ -291,7 +291,9 @@ ${consoleSections}
|
|
|
291
291
|
|
|
292
292
|
## \u{1F4CB} AI Agent Verification Checklist:
|
|
293
293
|
- [ ] **Console Errors**: ${consoleErrors.length === 0 ? "\u2705 No errors found" : `\u274C ${consoleErrors.length} errors \u2014 MUST REVIEW`}
|
|
294
|
+
- [ ] **Visual Layout Check**: Inspect snapshots (e.g. view_image on \`./01_${regularSnapshots[0]?.fileName || "quick_snap"}\`) for layout shifts or clipped elements.
|
|
294
295
|
- [ ] **Responsiveness at \`1024x768\`**: Elements do not overflow the screen, no unwanted horizontal scroll.
|
|
296
|
+
|
|
295
297
|
- [ ] **Typography & Spacing**: Spacing matches the design system and layout grids.
|
|
296
298
|
- [ ] **Color Palette & Theme**: Background tints and button accent colors match the concept.
|
|
297
299
|
- [ ] **Component States**: Modals open centered, dropdowns do not overlap with other layers (z-index).
|
|
@@ -316,8 +318,6 @@ ${consoleSections}
|
|
|
316
318
|
var ConsoleTracker = class {
|
|
317
319
|
entries = [];
|
|
318
320
|
attached = false;
|
|
319
|
-
/**
|
|
320
|
-
*/
|
|
321
321
|
attach(page) {
|
|
322
322
|
if (this.attached) return;
|
|
323
323
|
this.attached = true;
|
|
@@ -386,8 +386,6 @@ var ConsoleTracker = class {
|
|
|
386
386
|
return "log";
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
|
-
/**
|
|
390
|
-
*/
|
|
391
389
|
isIgnoredWarning(text) {
|
|
392
390
|
const ignoredPatterns = [
|
|
393
391
|
"findDOMNode is deprecated",
|
|
@@ -412,8 +410,28 @@ var import_fs3 = __toESM(require("fs"));
|
|
|
412
410
|
|
|
413
411
|
// src/shared/lib/playwrightLoader.ts
|
|
414
412
|
var import_playwright = require("playwright");
|
|
413
|
+
var chromium = new Proxy(import_playwright.chromium, {
|
|
414
|
+
get(target, prop, receiver) {
|
|
415
|
+
if (prop === "launch") {
|
|
416
|
+
return async (...args2) => {
|
|
417
|
+
try {
|
|
418
|
+
return await target.launch(...args2);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (err.message?.includes("Executable doesn't exist") || err.message?.includes("playwright install") || err.message?.includes("browser has not been downloaded")) {
|
|
421
|
+
console.error("\n\u274C [AgentLens] Playwright Chromium browser binary is missing!");
|
|
422
|
+
console.error("\u{1F449} Please install it by running: npx playwright install chromium\n");
|
|
423
|
+
}
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const val = Reflect.get(target, prop, receiver);
|
|
429
|
+
return typeof val === "function" ? val.bind(target) : val;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
415
432
|
|
|
416
433
|
// src/shared/drivers/desktopDriver.ts
|
|
434
|
+
var import_tree_kill = __toESM(require("tree-kill"));
|
|
417
435
|
var DesktopDriver = class {
|
|
418
436
|
port;
|
|
419
437
|
executablePath;
|
|
@@ -487,6 +505,11 @@ ${this.processStderr.trim() || "(no stderr output)"}`
|
|
|
487
505
|
);
|
|
488
506
|
}
|
|
489
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
|
+
}
|
|
490
513
|
const mergedEnv = {
|
|
491
514
|
...process.env,
|
|
492
515
|
...this.env,
|
|
@@ -495,6 +518,16 @@ ${this.processStderr.trim() || "(no stderr output)"}`
|
|
|
495
518
|
this.processStderr = "";
|
|
496
519
|
this.processExited = false;
|
|
497
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;
|
|
498
531
|
this.childProcess = (0, import_child_process.spawn)(resolvedExe, this.args, {
|
|
499
532
|
env: mergedEnv,
|
|
500
533
|
cwd: this.cwd || import_path3.default.dirname(resolvedExe),
|
|
@@ -517,7 +550,7 @@ ${this.processStderr.trim() || "(no stderr output)"}`
|
|
|
517
550
|
console.log(`[DesktopDriver] Attached to already running process on port ${this.port}`);
|
|
518
551
|
}
|
|
519
552
|
console.log(`[DesktopDriver] Connecting Playwright CDP to http://127.0.0.1:${this.port}...`);
|
|
520
|
-
this.browser = await
|
|
553
|
+
this.browser = await chromium.connectOverCDP(`http://127.0.0.1:${this.port}`);
|
|
521
554
|
const contexts = this.browser.contexts();
|
|
522
555
|
this.context = contexts[0] || await this.browser.newContext();
|
|
523
556
|
const pages = this.context.pages();
|
|
@@ -538,16 +571,12 @@ ${this.processStderr.trim() || "(no stderr output)"}`
|
|
|
538
571
|
});
|
|
539
572
|
this.browser = null;
|
|
540
573
|
}
|
|
541
|
-
if (this.childProcess && !this.childProcess.killed) {
|
|
542
|
-
console.log("[DesktopDriver] Terminating spawned desktop process...");
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.childProcess.kill("SIGTERM");
|
|
548
|
-
}
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
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
|
+
});
|
|
551
580
|
this.childProcess = null;
|
|
552
581
|
}
|
|
553
582
|
}
|
|
@@ -628,62 +657,65 @@ function generateMockIpcScript(registry) {
|
|
|
628
657
|
}
|
|
629
658
|
};
|
|
630
659
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
window.external
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const mock = window.__visualRunnerMocks[action];
|
|
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];
|
|
643
671
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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);
|
|
655
686
|
}
|
|
656
|
-
}
|
|
657
|
-
console.error('[Mock IPC] Failed to process legacy message:', e);
|
|
658
|
-
}
|
|
659
|
-
};
|
|
687
|
+
};
|
|
660
688
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
+
}
|
|
664
695
|
|
|
665
|
-
console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys(window.__visualRunnerMocks).length, 'mocked actions');
|
|
696
|
+
console.log('[Visual Runner] Mock IPC bridge initialized with', Object.keys((window as any).__visualRunnerMocks).length, 'mocked actions');
|
|
666
697
|
})();
|
|
698
|
+
|
|
667
699
|
`;
|
|
668
700
|
}
|
|
669
701
|
|
|
670
702
|
// src/shared/lib/config.ts
|
|
671
|
-
var import_fs4 = __toESM(require("fs"));
|
|
672
703
|
var import_path4 = __toESM(require("path"));
|
|
704
|
+
var import_fs4 = __toESM(require("fs"));
|
|
673
705
|
function loadConfig(cwd = process.cwd()) {
|
|
674
|
-
const
|
|
675
|
-
if (import_fs4.default.existsSync(
|
|
706
|
+
const jsonConfigPath = import_path4.default.join(cwd, "agent-lens.json");
|
|
707
|
+
if (import_fs4.default.existsSync(jsonConfigPath)) {
|
|
676
708
|
try {
|
|
677
|
-
const raw = import_fs4.default.readFileSync(
|
|
709
|
+
const raw = import_fs4.default.readFileSync(jsonConfigPath, "utf-8");
|
|
678
710
|
return JSON.parse(raw);
|
|
679
711
|
} catch (e) {
|
|
680
|
-
console.warn(`\u26A0\uFE0F
|
|
712
|
+
console.warn(`\u26A0\uFE0F [Config] Failed to parse agent-lens.json: ${e.message}`);
|
|
681
713
|
}
|
|
682
714
|
}
|
|
683
|
-
const
|
|
684
|
-
if (import_fs4.default.existsSync(
|
|
715
|
+
const packageJsonPath = import_path4.default.join(cwd, "package.json");
|
|
716
|
+
if (import_fs4.default.existsSync(packageJsonPath)) {
|
|
685
717
|
try {
|
|
686
|
-
const raw = import_fs4.default.readFileSync(
|
|
718
|
+
const raw = import_fs4.default.readFileSync(packageJsonPath, "utf-8");
|
|
687
719
|
const pkg = JSON.parse(raw);
|
|
688
720
|
if (pkg.agentLens && typeof pkg.agentLens === "object") {
|
|
689
721
|
return pkg.agentLens;
|
|
@@ -693,78 +725,60 @@ function loadConfig(cwd = process.cwd()) {
|
|
|
693
725
|
}
|
|
694
726
|
return {};
|
|
695
727
|
}
|
|
696
|
-
function
|
|
697
|
-
if (
|
|
698
|
-
|
|
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
|
+
}
|
|
699
734
|
}
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
"web/dist",
|
|
708
|
-
"ui/dist"
|
|
709
|
-
];
|
|
710
|
-
for (const rel of standardDirs) {
|
|
711
|
-
const candidate = import_path4.default.resolve(cwd, rel);
|
|
712
|
-
if (import_fs4.default.existsSync(candidate) && import_fs4.default.existsSync(import_path4.default.join(candidate, "index.html"))) {
|
|
713
|
-
return candidate;
|
|
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 {
|
|
714
742
|
}
|
|
715
743
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return foundDeepWwwroot;
|
|
744
|
+
if (rootHasDevScript) {
|
|
745
|
+
return void 0;
|
|
719
746
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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}`;
|
|
724
752
|
}
|
|
725
753
|
}
|
|
726
|
-
return
|
|
754
|
+
return void 0;
|
|
727
755
|
}
|
|
728
|
-
function
|
|
729
|
-
if (
|
|
730
|
-
return import_path4.default.resolve(
|
|
731
|
-
}
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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;
|
|
743
773
|
}
|
|
744
774
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
try {
|
|
750
|
-
const entries = import_fs4.default.readdirSync(dir, { withFileTypes: true });
|
|
751
|
-
for (const entry of entries) {
|
|
752
|
-
if (entry.isDirectory()) {
|
|
753
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "artifacts") {
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
const subDir = import_path4.default.join(dir, entry.name);
|
|
757
|
-
if (entry.name === "wwwroot" && import_fs4.default.existsSync(import_path4.default.join(subDir, "index.html"))) {
|
|
758
|
-
return subDir;
|
|
759
|
-
}
|
|
760
|
-
const found = findDeepIndexHtmlDir(subDir, maxDepth, currentDepth + 1);
|
|
761
|
-
if (found) return found;
|
|
762
|
-
}
|
|
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;
|
|
763
779
|
}
|
|
764
|
-
} catch {
|
|
765
|
-
return null;
|
|
766
780
|
}
|
|
767
|
-
return
|
|
781
|
+
return import_path4.default.resolve(process.cwd(), "dist");
|
|
768
782
|
}
|
|
769
783
|
|
|
770
784
|
// src/shared/drivers/previewDriver.ts
|
|
@@ -791,6 +805,7 @@ var PreviewDriver = class {
|
|
|
791
805
|
serverPort = 0;
|
|
792
806
|
options;
|
|
793
807
|
_baseUrl = "";
|
|
808
|
+
initialRouteMocks = [];
|
|
794
809
|
mockRegistry;
|
|
795
810
|
constructor(options2) {
|
|
796
811
|
this.options = options2 || {};
|
|
@@ -847,7 +862,7 @@ var PreviewDriver = class {
|
|
|
847
862
|
this._baseUrl = targetUrl;
|
|
848
863
|
console.log(`\u{1F310} [PreviewDriver] Connecting directly to live URL: ${targetUrl}`);
|
|
849
864
|
}
|
|
850
|
-
this.browser = await
|
|
865
|
+
this.browser = await chromium.launch({
|
|
851
866
|
headless: !this.options.headed,
|
|
852
867
|
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
853
868
|
});
|
|
@@ -860,9 +875,41 @@ var PreviewDriver = class {
|
|
|
860
875
|
await this.context.addInitScript(mockScript);
|
|
861
876
|
}
|
|
862
877
|
this.page = await this.context.newPage();
|
|
878
|
+
if (this.initialRouteMocks.length > 0) {
|
|
879
|
+
for (const entry of this.initialRouteMocks) {
|
|
880
|
+
await this.addRouteMock(entry);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
863
883
|
await this.page.goto(targetUrl, { waitUntil: "domcontentloaded" });
|
|
864
884
|
return { page: this.page, context: this.context, browser: this.browser };
|
|
865
885
|
}
|
|
886
|
+
async addRouteMock(entry) {
|
|
887
|
+
if (!this.page) {
|
|
888
|
+
this.initialRouteMocks.push(entry);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
await this.page.route(entry.url, async (route) => {
|
|
892
|
+
const req = route.request();
|
|
893
|
+
if (entry.method && req.method().toUpperCase() !== entry.method.toUpperCase()) {
|
|
894
|
+
return route.continue();
|
|
895
|
+
}
|
|
896
|
+
if (entry.delayMs) {
|
|
897
|
+
await new Promise((r) => setTimeout(r, entry.delayMs));
|
|
898
|
+
}
|
|
899
|
+
const isJson = typeof entry.body === "object" && entry.body !== null;
|
|
900
|
+
await route.fulfill({
|
|
901
|
+
status: entry.status ?? 200,
|
|
902
|
+
contentType: isJson ? "application/json" : "text/plain; charset=utf-8",
|
|
903
|
+
body: isJson ? JSON.stringify(entry.body) : String(entry.body ?? ""),
|
|
904
|
+
headers: entry.headers
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
async setupRouteMocks(routes) {
|
|
909
|
+
for (const r of routes) {
|
|
910
|
+
await this.addRouteMock(r);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
866
913
|
async updateMockIpc(action, data, options2) {
|
|
867
914
|
if (!this.page) {
|
|
868
915
|
throw new Error("PreviewDriver not started. Call start() first.");
|
|
@@ -907,104 +954,75 @@ var PreviewDriver = class {
|
|
|
907
954
|
|
|
908
955
|
// src/shared/lib/processManager.ts
|
|
909
956
|
var import_child_process2 = require("child_process");
|
|
910
|
-
var
|
|
957
|
+
var import_tree_kill2 = __toESM(require("tree-kill"));
|
|
911
958
|
var ProcessManager = class {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
stdoutOutput = "";
|
|
915
|
-
hasExited = false;
|
|
916
|
-
exitCode = null;
|
|
917
|
-
/**
|
|
918
|
-
* Spawns a background process (e.g. dev server, backend, or app)
|
|
919
|
-
*/
|
|
959
|
+
childProcess = null;
|
|
960
|
+
isStopped = false;
|
|
920
961
|
async start(command, options2) {
|
|
921
|
-
this.stderrOutput = "";
|
|
922
|
-
this.stdoutOutput = "";
|
|
923
|
-
this.hasExited = false;
|
|
924
|
-
this.exitCode = null;
|
|
925
962
|
const cwd = options2?.cwd || process.cwd();
|
|
926
|
-
const env = { ...process.env, ...options2?.env };
|
|
927
963
|
console.log(`\u{1F680} [ProcessManager] Starting command: "${command}" in ${cwd}`);
|
|
928
|
-
this.
|
|
964
|
+
this.childProcess = (0, import_child_process2.spawn)(command, {
|
|
929
965
|
cwd,
|
|
930
|
-
env,
|
|
931
|
-
shell:
|
|
966
|
+
env: { ...process.env, ...options2?.env },
|
|
967
|
+
shell: true,
|
|
932
968
|
stdio: ["ignore", "pipe", "pipe"]
|
|
933
969
|
});
|
|
934
|
-
this.
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
this.child.stderr?.on("data", (chunk) => {
|
|
939
|
-
const str = chunk.toString();
|
|
940
|
-
this.stderrOutput += str;
|
|
970
|
+
this.childProcess.stdout?.on("data", (chunk) => {
|
|
971
|
+
const line = chunk.toString().trim();
|
|
972
|
+
if (line) {
|
|
973
|
+
}
|
|
941
974
|
});
|
|
942
|
-
this.
|
|
943
|
-
|
|
944
|
-
|
|
975
|
+
this.childProcess.stderr?.on("data", (chunk) => {
|
|
976
|
+
const line = chunk.toString().trim();
|
|
977
|
+
if (line && !line.includes("ExperimentalWarning")) {
|
|
978
|
+
}
|
|
945
979
|
});
|
|
946
|
-
this.
|
|
947
|
-
|
|
980
|
+
this.childProcess.on("exit", (code, signal) => {
|
|
981
|
+
if (!this.isStopped && code !== 0 && code !== null) {
|
|
982
|
+
console.warn(`\u26A0\uFE0F [ProcessManager] Subprocess exited prematurely with code ${code}, signal ${signal}`);
|
|
983
|
+
}
|
|
948
984
|
});
|
|
949
985
|
}
|
|
950
|
-
/**
|
|
951
|
-
* Polls a URL until it starts responding or until timeout is reached.
|
|
952
|
-
*/
|
|
953
986
|
async waitForUrl(url, timeoutMs = 3e4) {
|
|
987
|
+
console.log(`\u23F3 [ProcessManager] Waiting for ${url} to respond...`);
|
|
954
988
|
const startTime = Date.now();
|
|
955
|
-
console.log(`\u23F3 [ProcessManager] Waiting for URL to become available: ${url} (timeout: ${timeoutMs / 1e3}s)...`);
|
|
956
989
|
while (Date.now() - startTime < timeoutMs) {
|
|
957
|
-
if (this.
|
|
990
|
+
if (this.childProcess && this.childProcess.exitCode !== null) {
|
|
958
991
|
throw new Error(
|
|
959
|
-
`[ProcessManager]
|
|
960
|
-
Stderr:
|
|
961
|
-
${this.stderrOutput.trim() || "(no stderr)"}
|
|
962
|
-
Stdout:
|
|
963
|
-
${this.stdoutOutput.slice(-500).trim()}`
|
|
992
|
+
`[ProcessManager] Server process exited with code ${this.childProcess.exitCode} while waiting for ${url}`
|
|
964
993
|
);
|
|
965
994
|
}
|
|
966
995
|
try {
|
|
967
|
-
const response = await fetch(url, {
|
|
996
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
968
997
|
if (response.status) {
|
|
969
|
-
console.log(`\u2705 [ProcessManager]
|
|
970
|
-
return;
|
|
998
|
+
console.log(`\u2705 [ProcessManager] Target ${url} is ready (status: ${response.status})!`);
|
|
999
|
+
return true;
|
|
971
1000
|
}
|
|
972
1001
|
} catch {
|
|
973
1002
|
}
|
|
974
|
-
await new Promise((
|
|
1003
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
975
1004
|
}
|
|
976
|
-
throw new Error(
|
|
977
|
-
`[ProcessManager] Timeout (${timeoutMs / 1e3}s) waiting for server at ${url}.
|
|
978
|
-
Last stdout:
|
|
979
|
-
${this.stdoutOutput.slice(-500).trim()}
|
|
980
|
-
Last stderr:
|
|
981
|
-
${this.stderrOutput.trim()}`
|
|
982
|
-
);
|
|
1005
|
+
throw new Error(`[ProcessManager] Timeout after ${timeoutMs}ms waiting for ${url} to respond.`);
|
|
983
1006
|
}
|
|
984
|
-
/**
|
|
985
|
-
* Gracefully and forcefully kills the process and all its children.
|
|
986
|
-
*/
|
|
987
1007
|
async stop() {
|
|
988
|
-
if (
|
|
989
|
-
this.child = null;
|
|
1008
|
+
if (this.isStopped || !this.childProcess || !this.childProcess.pid) {
|
|
990
1009
|
return;
|
|
991
1010
|
}
|
|
992
|
-
|
|
993
|
-
|
|
1011
|
+
this.isStopped = true;
|
|
1012
|
+
const pid = this.childProcess.pid;
|
|
1013
|
+
console.log(`\u{1F6D1} [ProcessManager] Terminating process tree for PID ${pid}...`);
|
|
994
1014
|
await new Promise((resolve) => {
|
|
995
|
-
(0,
|
|
1015
|
+
(0, import_tree_kill2.default)(pid, "SIGTERM", (err) => {
|
|
996
1016
|
if (err) {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
} catch {
|
|
1001
|
-
}
|
|
1017
|
+
try {
|
|
1018
|
+
(0, import_tree_kill2.default)(pid, "SIGKILL");
|
|
1019
|
+
} catch {
|
|
1002
1020
|
}
|
|
1003
1021
|
}
|
|
1004
1022
|
resolve();
|
|
1005
1023
|
});
|
|
1006
1024
|
});
|
|
1007
|
-
this.
|
|
1025
|
+
this.childProcess = null;
|
|
1008
1026
|
}
|
|
1009
1027
|
};
|
|
1010
1028
|
|
|
@@ -1075,9 +1093,16 @@ async function runVisualScenario(options2) {
|
|
|
1075
1093
|
console.log(`\u{1F4E6} [Mock IPC] Applying ${mergedMocks.length} mocks`);
|
|
1076
1094
|
previewDriver.mockRegistry.setBatch(mergedMocks);
|
|
1077
1095
|
}
|
|
1096
|
+
if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
|
|
1097
|
+
console.log(`\u{1F310} [Mock Network] Queuing ${scenario.mockRoutes.length} route mock(s)`);
|
|
1098
|
+
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
1099
|
+
}
|
|
1078
1100
|
const res = await previewDriver.start(currentViewport);
|
|
1079
1101
|
page = res.page;
|
|
1080
1102
|
context = res.context;
|
|
1103
|
+
if (scenario.mockRoutes && scenario.mockRoutes.length > 0) {
|
|
1104
|
+
await previewDriver.setupRouteMocks(scenario.mockRoutes);
|
|
1105
|
+
}
|
|
1081
1106
|
}
|
|
1082
1107
|
consoleTracker.attach(page);
|
|
1083
1108
|
console.log(`\u{1F50D} [Console Tracker] Attached \u2014 errors and warnings will be captured
|
|
@@ -1223,6 +1248,25 @@ async function runVisualScenario(options2) {
|
|
|
1223
1248
|
console.log(`\u{1F4E6} [Mock IPC] Set ${action} -> ${typeof data === "string" ? data : JSON.stringify(data).slice(0, 80)}...`);
|
|
1224
1249
|
await previewDriver.updateMockIpc(action, data, mockOptions);
|
|
1225
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
|
+
},
|
|
1226
1270
|
getConsoleErrors: () => consoleTracker.getErrors(),
|
|
1227
1271
|
getConsoleWarnings: () => consoleTracker.getWarnings(),
|
|
1228
1272
|
hasConsoleErrors: () => consoleTracker.hasErrors,
|
|
@@ -1415,6 +1459,7 @@ function parseViewportPresets(raw) {
|
|
|
1415
1459
|
}
|
|
1416
1460
|
return presets.length > 0 ? presets : [VIEWPORT_PRESETS.DEFAULT, PRESET_MAP.mobile];
|
|
1417
1461
|
}
|
|
1462
|
+
var CANDIDATE_PORTS = [5173, 3e3, 4321, 4200, 8080, 8e3, 3001];
|
|
1418
1463
|
async function isPortResponding(url) {
|
|
1419
1464
|
try {
|
|
1420
1465
|
const res = await fetch(url, { signal: AbortSignal.timeout(800) });
|
|
@@ -1423,17 +1468,24 @@ async function isPortResponding(url) {
|
|
|
1423
1468
|
return false;
|
|
1424
1469
|
}
|
|
1425
1470
|
}
|
|
1471
|
+
async function detectActiveDevServer() {
|
|
1472
|
+
for (const port of CANDIDATE_PORTS) {
|
|
1473
|
+
const url = `http://localhost:${port}`;
|
|
1474
|
+
if (await isPortResponding(url)) {
|
|
1475
|
+
return url;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1426
1480
|
async function runQuickSnap(options2) {
|
|
1427
1481
|
let targetUrl = options2.url;
|
|
1428
1482
|
let useStaticPreview = false;
|
|
1429
1483
|
let wwwrootDir;
|
|
1430
1484
|
if (!targetUrl && !options2.start) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
targetUrl = "http://localhost:3000";
|
|
1436
|
-
console.log(`\u{1F310} [Quick Snap] Detected active dev server on http://localhost:3000`);
|
|
1485
|
+
const activeUrl = await detectActiveDevServer();
|
|
1486
|
+
if (activeUrl) {
|
|
1487
|
+
targetUrl = activeUrl;
|
|
1488
|
+
console.log(`\u{1F310} [Quick Snap] Detected active dev server on ${targetUrl}`);
|
|
1437
1489
|
} else {
|
|
1438
1490
|
const candidateDir = resolveWwwrootDir();
|
|
1439
1491
|
if (import_fs7.default.existsSync(candidateDir) && import_fs7.default.existsSync(import_path7.default.join(candidateDir, "index.html"))) {
|
|
@@ -1442,7 +1494,7 @@ async function runQuickSnap(options2) {
|
|
|
1442
1494
|
console.log(`\u{1F4E6} [Quick Snap] No active server found. Detected built static directory at "${candidateDir}". Launching static preview...`);
|
|
1443
1495
|
} else {
|
|
1444
1496
|
console.error(`
|
|
1445
|
-
\u274C [Quick Snap] No active server found on
|
|
1497
|
+
\u274C [Quick Snap] No active server found on common ports (${CANDIDATE_PORTS.join(", ")}), and no static build found.`);
|
|
1446
1498
|
console.log(`\u{1F4A1} Suggested actions:`);
|
|
1447
1499
|
console.log(` 1. Pass a start command: npx agent-lens snap --start="npm run dev"`);
|
|
1448
1500
|
console.log(` 2. Specify your URL: npx agent-lens snap --url=http://localhost:8080`);
|
|
@@ -1583,15 +1635,19 @@ for (const arg of effectiveArgs) {
|
|
|
1583
1635
|
} else if (arg.startsWith("--selector=")) {
|
|
1584
1636
|
options.selector = arg.split("=")[1];
|
|
1585
1637
|
} else if (arg.startsWith("--viewports=")) {
|
|
1586
|
-
|
|
1638
|
+
const raw = arg.slice("--viewports=".length);
|
|
1639
|
+
options.viewports = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1587
1640
|
} else if (arg.startsWith("--wait=")) {
|
|
1588
|
-
options.waitMs = parseInt(arg.
|
|
1641
|
+
options.waitMs = parseInt(arg.slice("--wait=".length), 10);
|
|
1589
1642
|
} else if (arg.startsWith("--name=")) {
|
|
1590
|
-
options.name = arg.
|
|
1591
|
-
} else if (arg.startsWith("--exe=")
|
|
1592
|
-
options.exe = arg.
|
|
1643
|
+
options.name = arg.slice("--name=".length);
|
|
1644
|
+
} else if (arg.startsWith("--exe=")) {
|
|
1645
|
+
options.exe = arg.slice("--exe=".length);
|
|
1646
|
+
} else if (arg.startsWith("--executable=")) {
|
|
1647
|
+
options.exe = arg.slice("--executable=".length);
|
|
1593
1648
|
} else if (arg.startsWith("--clean=") || arg.startsWith("--cleanup=")) {
|
|
1594
|
-
const
|
|
1649
|
+
const prefix = arg.startsWith("--clean=") ? "--clean=" : "--cleanup=";
|
|
1650
|
+
const rawPaths = arg.slice(prefix.length);
|
|
1595
1651
|
options.clean = rawPaths.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1596
1652
|
} else if (arg.startsWith("--dir=")) {
|
|
1597
1653
|
options.dir = arg.split("=")[1];
|
|
@@ -1777,9 +1833,8 @@ async function main() {
|
|
|
1777
1833
|
const buildCmd = typeof options.build === "string" ? options.build : fileConfig.buildCommand || "npm run build";
|
|
1778
1834
|
console.log(`
|
|
1779
1835
|
\u{1F528} [Build] Running build process: "${buildCmd}"...`);
|
|
1780
|
-
const { execSync } = require("child_process");
|
|
1781
1836
|
try {
|
|
1782
|
-
execSync(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
|
|
1837
|
+
(0, import_child_process3.execSync)(buildCmd, { stdio: "inherit", cwd: options.startCwd || process.cwd() });
|
|
1783
1838
|
} catch (e) {
|
|
1784
1839
|
console.error(`\u274C Build failed: ${buildCmd}`);
|
|
1785
1840
|
process.exit(1);
|