@everydaydevopsio/pilot 0.5.0 → 0.6.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/CHANGELOG.md +64 -0
- package/CLAUDE.md +9 -9
- package/README.md +108 -53
- package/dist/browser/browser-manager.d.ts +60 -0
- package/dist/browser/browser-manager.d.ts.map +1 -0
- package/dist/browser/browser-manager.js +348 -0
- package/dist/browser/browser-manager.js.map +1 -0
- package/dist/browser/chrome-launcher.d.ts +30 -0
- package/dist/browser/chrome-launcher.d.ts.map +1 -0
- package/dist/browser/chrome-launcher.js +217 -0
- package/dist/browser/chrome-launcher.js.map +1 -0
- package/dist/browser/connection.d.ts +23 -0
- package/dist/browser/connection.d.ts.map +1 -0
- package/dist/browser/connection.js +53 -0
- package/dist/browser/connection.js.map +1 -0
- package/dist/browser/events.d.ts +9 -0
- package/dist/browser/events.d.ts.map +1 -0
- package/dist/browser/events.js +99 -0
- package/dist/browser/events.js.map +1 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/inspect/element-ref.d.ts +23 -0
- package/dist/browser/inspect/element-ref.d.ts.map +1 -0
- package/dist/browser/inspect/element-ref.js +52 -0
- package/dist/browser/inspect/element-ref.js.map +1 -0
- package/dist/browser/inspect/snapshot.d.ts +11 -0
- package/dist/browser/inspect/snapshot.d.ts.map +1 -0
- package/dist/browser/inspect/snapshot.js +282 -0
- package/dist/browser/inspect/snapshot.js.map +1 -0
- package/dist/browser/inspect/styles.d.ts +45 -0
- package/dist/browser/inspect/styles.d.ts.map +1 -0
- package/dist/browser/inspect/styles.js +190 -0
- package/dist/browser/inspect/styles.js.map +1 -0
- package/dist/browser/inspect/types.d.ts +35 -0
- package/dist/browser/inspect/types.d.ts.map +1 -0
- package/dist/browser/inspect/types.js +2 -0
- package/dist/browser/inspect/types.js.map +1 -0
- package/dist/browser/interaction/dialogs.d.ts +18 -0
- package/dist/browser/interaction/dialogs.d.ts.map +1 -0
- package/dist/browser/interaction/dialogs.js +35 -0
- package/dist/browser/interaction/dialogs.js.map +1 -0
- package/dist/browser/interaction/downloads.d.ts +23 -0
- package/dist/browser/interaction/downloads.d.ts.map +1 -0
- package/dist/browser/interaction/downloads.js +87 -0
- package/dist/browser/interaction/downloads.js.map +1 -0
- package/dist/browser/interaction/events.d.ts +17 -0
- package/dist/browser/interaction/events.d.ts.map +1 -0
- package/dist/browser/interaction/events.js +66 -0
- package/dist/browser/interaction/events.js.map +1 -0
- package/dist/browser/interaction/ref-resolver.d.ts +18 -0
- package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
- package/dist/browser/interaction/ref-resolver.js +32 -0
- package/dist/browser/interaction/ref-resolver.js.map +1 -0
- package/dist/browser/interaction/uploads.d.ts +4 -0
- package/dist/browser/interaction/uploads.d.ts.map +1 -0
- package/dist/browser/interaction/uploads.js +43 -0
- package/dist/browser/interaction/uploads.js.map +1 -0
- package/dist/browser/network/network-buffer.d.ts +19 -0
- package/dist/browser/network/network-buffer.d.ts.map +1 -0
- package/dist/browser/network/network-buffer.js +118 -0
- package/dist/browser/network/network-buffer.js.map +1 -0
- package/dist/browser/network/network-monitor.d.ts +8 -0
- package/dist/browser/network/network-monitor.d.ts.map +1 -0
- package/dist/browser/network/network-monitor.js +85 -0
- package/dist/browser/network/network-monitor.js.map +1 -0
- package/dist/browser/network/redaction.d.ts +3 -0
- package/dist/browser/network/redaction.d.ts.map +1 -0
- package/dist/browser/network/redaction.js +36 -0
- package/dist/browser/network/redaction.js.map +1 -0
- package/dist/browser/network/types.d.ts +45 -0
- package/dist/browser/network/types.d.ts.map +1 -0
- package/dist/browser/network/types.js +2 -0
- package/dist/browser/network/types.js.map +1 -0
- package/dist/browser/performance/analyze.d.ts +5 -0
- package/dist/browser/performance/analyze.d.ts.map +1 -0
- package/dist/browser/performance/analyze.js +230 -0
- package/dist/browser/performance/analyze.js.map +1 -0
- package/dist/browser/performance/trace.d.ts +10 -0
- package/dist/browser/performance/trace.d.ts.map +1 -0
- package/dist/browser/performance/trace.js +38 -0
- package/dist/browser/performance/trace.js.map +1 -0
- package/dist/browser/performance/types.d.ts +31 -0
- package/dist/browser/performance/types.d.ts.map +1 -0
- package/dist/browser/performance/types.js +2 -0
- package/dist/browser/performance/types.js.map +1 -0
- package/dist/browser/security/origins.d.ts +18 -0
- package/dist/browser/security/origins.d.ts.map +1 -0
- package/dist/browser/security/origins.js +84 -0
- package/dist/browser/security/origins.js.map +1 -0
- package/dist/browser/tabs.d.ts +15 -0
- package/dist/browser/tabs.d.ts.map +1 -0
- package/dist/browser/tabs.js +71 -0
- package/dist/browser/tabs.js.map +1 -0
- package/dist/browser/types.d.ts +31 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/browser.d.ts +2 -99
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -653
- package/dist/browser.js.map +1 -1
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +38 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/click.d.ts +3 -1
- package/dist/commands/click.d.ts.map +1 -1
- package/dist/commands/click.js +16 -4
- package/dist/commands/click.js.map +1 -1
- package/dist/commands/connect.d.ts +14 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +19 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/dialog.d.ts +19 -0
- package/dist/commands/dialog.d.ts.map +1 -0
- package/dist/commands/dialog.js +51 -0
- package/dist/commands/dialog.js.map +1 -0
- package/dist/commands/downloads.d.ts +30 -0
- package/dist/commands/downloads.d.ts.map +1 -0
- package/dist/commands/downloads.js +66 -0
- package/dist/commands/downloads.js.map +1 -0
- package/dist/commands/fill.d.ts +16 -0
- package/dist/commands/fill.d.ts.map +1 -0
- package/dist/commands/fill.js +15 -0
- package/dist/commands/fill.js.map +1 -0
- package/dist/commands/find.d.ts +14 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +26 -0
- package/dist/commands/find.js.map +1 -0
- package/dist/commands/hover.d.ts +13 -0
- package/dist/commands/hover.d.ts.map +1 -0
- package/dist/commands/hover.js +24 -0
- package/dist/commands/hover.js.map +1 -0
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +13 -0
- package/dist/commands/navigate.js.map +1 -1
- package/dist/commands/network.d.ts +33 -0
- package/dist/commands/network.d.ts.map +1 -0
- package/dist/commands/network.js +84 -0
- package/dist/commands/network.js.map +1 -0
- package/dist/commands/performance.d.ts +12 -0
- package/dist/commands/performance.d.ts.map +1 -0
- package/dist/commands/performance.js +26 -0
- package/dist/commands/performance.js.map +1 -0
- package/dist/commands/press_key.d.ts +10 -0
- package/dist/commands/press_key.d.ts.map +1 -0
- package/dist/commands/press_key.js +103 -0
- package/dist/commands/press_key.js.map +1 -0
- package/dist/commands/scroll.d.ts +21 -0
- package/dist/commands/scroll.d.ts.map +1 -0
- package/dist/commands/scroll.js +54 -0
- package/dist/commands/scroll.js.map +1 -0
- package/dist/commands/select.d.ts +18 -0
- package/dist/commands/select.d.ts.map +1 -0
- package/dist/commands/select.js +53 -0
- package/dist/commands/select.js.map +1 -0
- package/dist/commands/snapshot.d.ts +9 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +7 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/commands/styles.d.ts +13 -0
- package/dist/commands/styles.d.ts.map +1 -0
- package/dist/commands/styles.js +7 -0
- package/dist/commands/styles.js.map +1 -0
- package/dist/commands/type.d.ts +3 -1
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +11 -2
- package/dist/commands/type.js.map +1 -1
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +13 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/mcp/console-buffer.d.ts +9 -0
- package/dist/mcp/console-buffer.d.ts.map +1 -1
- package/dist/mcp/console-buffer.js.map +1 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/browser.d.ts.map +1 -1
- package/dist/mcp/tools/browser.js +99 -8
- package/dist/mcp/tools/browser.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts +5 -0
- package/dist/mcp/tools/connect.d.ts.map +1 -0
- package/dist/mcp/tools/connect.js +48 -0
- package/dist/mcp/tools/connect.js.map +1 -0
- package/dist/mcp/tools/files.d.ts +4 -0
- package/dist/mcp/tools/files.d.ts.map +1 -0
- package/dist/mcp/tools/files.js +85 -0
- package/dist/mcp/tools/files.js.map +1 -0
- package/dist/mcp/tools/interaction.d.ts +4 -0
- package/dist/mcp/tools/interaction.d.ts.map +1 -0
- package/dist/mcp/tools/interaction.js +176 -0
- package/dist/mcp/tools/interaction.js.map +1 -0
- package/dist/mcp/tools/names.d.ts +1 -1
- package/dist/mcp/tools/names.d.ts.map +1 -1
- package/dist/mcp/tools/names.js +19 -1
- package/dist/mcp/tools/names.js.map +1 -1
- package/dist/mcp/tools/network.d.ts +4 -0
- package/dist/mcp/tools/network.d.ts.map +1 -0
- package/dist/mcp/tools/network.js +68 -0
- package/dist/mcp/tools/network.js.map +1 -0
- package/dist/mcp/tools/performance.d.ts +4 -0
- package/dist/mcp/tools/performance.d.ts.map +1 -0
- package/dist/mcp/tools/performance.js +26 -0
- package/dist/mcp/tools/performance.js.map +1 -0
- package/dist/mcp/tools/runtime.d.ts +4 -0
- package/dist/mcp/tools/runtime.d.ts.map +1 -0
- package/dist/mcp/tools/runtime.js +128 -0
- package/dist/mcp/tools/runtime.js.map +1 -0
- package/dist/mcp/tools/snapshot.d.ts +4 -0
- package/dist/mcp/tools/snapshot.d.ts.map +1 -0
- package/dist/mcp/tools/snapshot.js +42 -0
- package/dist/mcp/tools/snapshot.js.map +1 -0
- package/dist/mcp/tools/styles.d.ts +4 -0
- package/dist/mcp/tools/styles.d.ts.map +1 -0
- package/dist/mcp/tools/styles.js +27 -0
- package/dist/mcp/tools/styles.js.map +1 -0
- package/dist/util/config.d.ts.map +1 -1
- package/dist/util/config.js +3 -2
- package/dist/util/config.js.map +1 -1
- package/package.json +7 -3
- package/src/browser/browser-manager.ts +457 -0
- package/src/browser/chrome-launcher.ts +271 -0
- package/src/browser/connection.ts +84 -0
- package/src/browser/events.ts +121 -0
- package/src/browser/index.ts +20 -0
- package/src/browser/inspect/element-ref.ts +68 -0
- package/src/browser/inspect/snapshot.ts +348 -0
- package/src/browser/inspect/styles.ts +258 -0
- package/src/browser/inspect/types.ts +37 -0
- package/src/browser/interaction/dialogs.ts +54 -0
- package/src/browser/interaction/downloads.ts +116 -0
- package/src/browser/interaction/events.ts +79 -0
- package/src/browser/interaction/ref-resolver.ts +53 -0
- package/src/browser/interaction/uploads.ts +60 -0
- package/src/browser/network/network-buffer.ts +133 -0
- package/src/browser/network/network-monitor.ts +107 -0
- package/src/browser/network/redaction.ts +42 -0
- package/src/browser/network/types.ts +47 -0
- package/src/browser/performance/analyze.ts +298 -0
- package/src/browser/performance/trace.ts +60 -0
- package/src/browser/performance/types.ts +34 -0
- package/src/browser/security/origins.ts +102 -0
- package/src/browser/tabs.ts +110 -0
- package/src/browser/types.ts +31 -0
- package/src/browser.ts +22 -798
- package/src/commands/check.ts +59 -0
- package/src/commands/click.ts +18 -4
- package/src/commands/connect.ts +40 -0
- package/src/commands/dialog.ts +79 -0
- package/src/commands/downloads.ts +111 -0
- package/src/commands/fill.ts +34 -0
- package/src/commands/find.ts +52 -0
- package/src/commands/hover.ts +42 -0
- package/src/commands/navigate.ts +20 -0
- package/src/commands/network.ts +141 -0
- package/src/commands/performance.ts +51 -0
- package/src/commands/press_key.ts +134 -0
- package/src/commands/scroll.ts +79 -0
- package/src/commands/select.ts +78 -0
- package/src/commands/snapshot.ts +18 -0
- package/src/commands/styles.ts +24 -0
- package/src/commands/type.ts +12 -2
- package/src/commands/upload.ts +36 -0
- package/src/mcp/console-buffer.ts +10 -0
- package/src/mcp/server.ts +74 -2
- package/src/mcp/tools/browser.ts +113 -7
- package/src/mcp/tools/connect.ts +62 -0
- package/src/mcp/tools/files.ts +120 -0
- package/src/mcp/tools/interaction.ts +232 -0
- package/src/mcp/tools/names.ts +19 -1
- package/src/mcp/tools/network.ts +86 -0
- package/src/mcp/tools/performance.ts +40 -0
- package/src/mcp/tools/runtime.ts +164 -0
- package/src/mcp/tools/snapshot.ts +64 -0
- package/src/mcp/tools/styles.ts +41 -0
- package/src/util/config.ts +3 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
2
|
+
import { existsSync, lstatSync, readlinkSync, unlinkSync } from 'fs';
|
|
3
|
+
import * as net from 'net';
|
|
4
|
+
import { homedir, hostname } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import CDP from 'chrome-remote-interface';
|
|
7
|
+
import { getLogger } from '../util/logger.js';
|
|
8
|
+
import type { SandboxDecision } from './types.js';
|
|
9
|
+
|
|
10
|
+
export const PROFILE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
11
|
+
|
|
12
|
+
export function sandboxDecision(): SandboxDecision {
|
|
13
|
+
const envOverride = process.env.PILOT_CHROME_NO_SANDBOX;
|
|
14
|
+
if (envOverride !== undefined) {
|
|
15
|
+
const truthy = envOverride === 'true' || envOverride === '1';
|
|
16
|
+
return truthy
|
|
17
|
+
? { disable: true, reason: 'env_override' }
|
|
18
|
+
: { disable: false };
|
|
19
|
+
}
|
|
20
|
+
if (process.platform !== 'linux' && process.platform !== 'win32') {
|
|
21
|
+
return { disable: false };
|
|
22
|
+
}
|
|
23
|
+
if (process.getuid?.() === 0) {
|
|
24
|
+
return { disable: true, reason: 'root_user' };
|
|
25
|
+
}
|
|
26
|
+
return { disable: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldDisableSandbox(): boolean {
|
|
30
|
+
return sandboxDecision().disable;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveUserDataDir(profileName: string): string {
|
|
34
|
+
if (!PROFILE_NAME_RE.test(profileName)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Invalid profile name "${profileName}". Use only letters, digits, hyphens, and underscores.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const xdgData =
|
|
40
|
+
process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
41
|
+
return join(xdgData, 'pilot', profileName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fileOrSymlinkExists(filePath: string): boolean {
|
|
45
|
+
try {
|
|
46
|
+
lstatSync(filePath);
|
|
47
|
+
return true;
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
50
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function extractLockPid(
|
|
58
|
+
lockPath: string
|
|
59
|
+
): { pid: number; lockHost: string } | null {
|
|
60
|
+
try {
|
|
61
|
+
const stat = lstatSync(lockPath);
|
|
62
|
+
if (!stat.isSymbolicLink()) return null;
|
|
63
|
+
const target = readlinkSync(lockPath);
|
|
64
|
+
const lastDash = target.lastIndexOf('-');
|
|
65
|
+
if (lastDash === -1) return null;
|
|
66
|
+
const lockHost = target.slice(0, lastDash);
|
|
67
|
+
const pid = parseInt(target.slice(lastDash + 1), 10);
|
|
68
|
+
if (isNaN(pid) || pid <= 0) return null;
|
|
69
|
+
return { pid, lockHost };
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isProcessAlive(pid: number): boolean {
|
|
76
|
+
if (pid <= 0) return false;
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (err: unknown) {
|
|
81
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
82
|
+
return code !== 'ESRCH';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function removeStaleLocks(userDataDir: string): void {
|
|
87
|
+
for (const lockName of ['SingletonLock', 'lockfile']) {
|
|
88
|
+
try {
|
|
89
|
+
unlinkSync(join(userDataDir, lockName));
|
|
90
|
+
} catch (err: unknown) {
|
|
91
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
92
|
+
if (code !== 'ENOENT') throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const logger = getLogger();
|
|
96
|
+
logger.warn(
|
|
97
|
+
{ userDataDir },
|
|
98
|
+
'Cleaned up stale Chrome lock files from a previous crash'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isProfileLocked(userDataDir: string): boolean {
|
|
103
|
+
const singletonPath = join(userDataDir, 'SingletonLock');
|
|
104
|
+
const lockfilePath = join(userDataDir, 'lockfile');
|
|
105
|
+
|
|
106
|
+
const hasSingleton = fileOrSymlinkExists(singletonPath);
|
|
107
|
+
const hasLockfile = existsSync(lockfilePath);
|
|
108
|
+
|
|
109
|
+
if (!hasSingleton && !hasLockfile) return false;
|
|
110
|
+
|
|
111
|
+
const lockInfo = hasSingleton ? extractLockPid(singletonPath) : null;
|
|
112
|
+
|
|
113
|
+
if (!lockInfo) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentHost = hostname();
|
|
118
|
+
if (lockInfo.lockHost !== currentHost) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isProcessAlive(lockInfo.pid)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
removeStaleLocks(userDataDir);
|
|
128
|
+
return false;
|
|
129
|
+
} catch {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function findChromeExecutable(override?: string): string {
|
|
135
|
+
if (override) return override;
|
|
136
|
+
const env = process.env.PILOT_CHROME_PATH;
|
|
137
|
+
if (env) return env;
|
|
138
|
+
|
|
139
|
+
const platform = process.platform;
|
|
140
|
+
const candidates: string[] =
|
|
141
|
+
platform === 'darwin'
|
|
142
|
+
? [
|
|
143
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
144
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
145
|
+
]
|
|
146
|
+
: platform === 'linux'
|
|
147
|
+
? [
|
|
148
|
+
'/usr/bin/google-chrome-stable',
|
|
149
|
+
'/usr/bin/google-chrome',
|
|
150
|
+
'/usr/bin/chromium-browser',
|
|
151
|
+
'/usr/bin/chromium',
|
|
152
|
+
'/snap/bin/chromium'
|
|
153
|
+
]
|
|
154
|
+
: platform === 'win32'
|
|
155
|
+
? [
|
|
156
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
157
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
|
|
158
|
+
]
|
|
159
|
+
: [];
|
|
160
|
+
|
|
161
|
+
for (const p of candidates) {
|
|
162
|
+
if (existsSync(p)) return p;
|
|
163
|
+
}
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Chrome not found on ${platform}. Install Google Chrome or set PILOT_CHROME_PATH.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function findFreePort(): Promise<number> {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const server = net.createServer();
|
|
172
|
+
server.listen(0, '127.0.0.1', () => {
|
|
173
|
+
const { port } = server.address() as net.AddressInfo;
|
|
174
|
+
server.close(() => resolve(port));
|
|
175
|
+
});
|
|
176
|
+
server.on('error', reject);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function waitForChromeReady(
|
|
181
|
+
port: number,
|
|
182
|
+
timeoutMs = 10000
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const deadline = Date.now() + timeoutMs;
|
|
185
|
+
while (Date.now() < deadline) {
|
|
186
|
+
try {
|
|
187
|
+
await CDP.List({ host: '127.0.0.1', port });
|
|
188
|
+
return;
|
|
189
|
+
} catch {
|
|
190
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Chrome did not become ready on port ${port} within ${timeoutMs}ms`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface ChromeLaunchResult {
|
|
199
|
+
process: ChildProcess;
|
|
200
|
+
port: number;
|
|
201
|
+
userDataDir: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function launchChrome(opts: {
|
|
205
|
+
chromePath: string;
|
|
206
|
+
port: number;
|
|
207
|
+
userDataDir: string;
|
|
208
|
+
headless: boolean;
|
|
209
|
+
windowWidth: number;
|
|
210
|
+
windowHeight: number;
|
|
211
|
+
}): ChildProcess {
|
|
212
|
+
const logger = getLogger();
|
|
213
|
+
|
|
214
|
+
const args = [
|
|
215
|
+
`--remote-debugging-port=${opts.port}`,
|
|
216
|
+
`--user-data-dir=${opts.userDataDir}`,
|
|
217
|
+
`--window-size=${opts.windowWidth},${opts.windowHeight}`,
|
|
218
|
+
'--no-first-run',
|
|
219
|
+
'--no-default-browser-check',
|
|
220
|
+
'--disable-default-apps',
|
|
221
|
+
'--disable-extensions'
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
if (opts.headless) {
|
|
225
|
+
args.push('--headless', '--disable-gpu');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (process.platform === 'linux') {
|
|
229
|
+
args.push(
|
|
230
|
+
'--disable-dev-shm-usage',
|
|
231
|
+
'--disable-software-rasterizer',
|
|
232
|
+
'--disable-breakpad',
|
|
233
|
+
'--password-store=basic',
|
|
234
|
+
'--use-mock-keychain'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sandbox = sandboxDecision();
|
|
239
|
+
if (sandbox.disable) {
|
|
240
|
+
args.push('--no-sandbox');
|
|
241
|
+
logger.warn(
|
|
242
|
+
{ reason: sandbox.reason },
|
|
243
|
+
'Chrome will run with --no-sandbox. The renderer sandbox is disabled; any page the agent visits runs with the same privileges as this process. Set PILOT_CHROME_NO_SANDBOX=false to override, or run as a non-root user to keep the sandbox enabled.'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
logger.info(
|
|
248
|
+
{
|
|
249
|
+
chromePath: opts.chromePath,
|
|
250
|
+
port: opts.port,
|
|
251
|
+
headless: opts.headless,
|
|
252
|
+
sandbox: sandbox.disable ? 'disabled' : 'enabled'
|
|
253
|
+
},
|
|
254
|
+
'Launching Chrome'
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return spawn(opts.chromePath, args, {
|
|
258
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
259
|
+
detached: false
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function cleanupLockFiles(userDataDir: string): void {
|
|
264
|
+
for (const lockName of ['SingletonLock', 'lockfile']) {
|
|
265
|
+
try {
|
|
266
|
+
unlinkSync(join(userDataDir, lockName));
|
|
267
|
+
} catch {
|
|
268
|
+
// lock file may not exist — that's fine
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import CDP from 'chrome-remote-interface';
|
|
2
|
+
import type { Client } from 'chrome-remote-interface';
|
|
3
|
+
import { getLogger } from '../util/logger.js';
|
|
4
|
+
import type { Config } from '../util/config.js';
|
|
5
|
+
import { type ViewportConfig, applyViewport } from '../viewport.js';
|
|
6
|
+
|
|
7
|
+
export interface ConnectionInfo {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function connectToChrome(opts: {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
}): Promise<{ client: Client; targetId: string; url: string }> {
|
|
16
|
+
const targets = await CDP.List({ host: opts.host, port: opts.port });
|
|
17
|
+
|
|
18
|
+
const pageTarget = targets.find((t) => t.type === 'page');
|
|
19
|
+
if (!pageTarget) {
|
|
20
|
+
throw new Error('No page target found in Chrome');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const client = await CDP({
|
|
24
|
+
host: opts.host,
|
|
25
|
+
port: opts.port,
|
|
26
|
+
target: pageTarget.id
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
client,
|
|
31
|
+
targetId: pageTarget.id,
|
|
32
|
+
url: pageTarget.url
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function enableDomains(
|
|
37
|
+
client: Client,
|
|
38
|
+
viewportConfig: ViewportConfig | null
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
await Promise.all([
|
|
41
|
+
client.Network.enable({}),
|
|
42
|
+
client.Console.enable(),
|
|
43
|
+
client.Page.enable(),
|
|
44
|
+
client.Runtime.enable()
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (viewportConfig) {
|
|
48
|
+
await applyViewport(client, viewportConfig);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getConnectionInfo(
|
|
53
|
+
config: Config,
|
|
54
|
+
launchedCdpPort: number | null
|
|
55
|
+
): ConnectionInfo {
|
|
56
|
+
return {
|
|
57
|
+
host: launchedCdpPort ? '127.0.0.1' : config.cdpHost,
|
|
58
|
+
port: launchedCdpPort ?? config.cdpPort
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function scheduleReconnect(
|
|
63
|
+
config: Config,
|
|
64
|
+
state: {
|
|
65
|
+
destroyed: boolean;
|
|
66
|
+
retryTimer: ReturnType<typeof setTimeout> | null;
|
|
67
|
+
retryMs: number;
|
|
68
|
+
},
|
|
69
|
+
reconnectFn: () => void
|
|
70
|
+
): ReturnType<typeof setTimeout> | null {
|
|
71
|
+
if (state.destroyed) return null;
|
|
72
|
+
if (state.retryTimer) return state.retryTimer;
|
|
73
|
+
|
|
74
|
+
const logger = getLogger();
|
|
75
|
+
logger.debug({ retryMs: state.retryMs }, 'Scheduling reconnect');
|
|
76
|
+
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
state.retryTimer = null;
|
|
79
|
+
state.retryMs = Math.min(state.retryMs * 2, config.cdpMaxRetryMs);
|
|
80
|
+
reconnectFn();
|
|
81
|
+
}, state.retryMs);
|
|
82
|
+
timer.unref();
|
|
83
|
+
return timer;
|
|
84
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Client } from 'chrome-remote-interface';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize CDP console level names to match ConsoleLevel.
|
|
5
|
+
* CDP uses "warning" for console.warn; normalize to "warn".
|
|
6
|
+
*/
|
|
7
|
+
function normLevel(raw: string): string {
|
|
8
|
+
return raw === 'warning' ? 'warn' : raw;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function attachEventListeners(
|
|
12
|
+
client: Client,
|
|
13
|
+
opts: {
|
|
14
|
+
emit: (event: string, data: unknown) => void;
|
|
15
|
+
onDisconnect: () => void;
|
|
16
|
+
pendingNetworkRequests: Set<string>;
|
|
17
|
+
checkNetworkIdle: () => void;
|
|
18
|
+
onFrameNavigated: (url: string) => void;
|
|
19
|
+
}
|
|
20
|
+
): void {
|
|
21
|
+
client.on('disconnect', () => {
|
|
22
|
+
opts.onDisconnect();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Console events
|
|
26
|
+
client.Runtime.consoleAPICalled((params) => {
|
|
27
|
+
const text = params.args
|
|
28
|
+
.map((a) =>
|
|
29
|
+
a.value !== undefined ? String(a.value) : (a.description ?? '')
|
|
30
|
+
)
|
|
31
|
+
.join(' ');
|
|
32
|
+
opts.emit('console_message', {
|
|
33
|
+
level: normLevel(params.type),
|
|
34
|
+
text,
|
|
35
|
+
url: params.stackTrace?.callFrames?.[0]?.url ?? '',
|
|
36
|
+
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber ?? 0,
|
|
37
|
+
timestamp: Date.now()
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
client.Console.messageAdded((params) => {
|
|
42
|
+
opts.emit('console_message', {
|
|
43
|
+
level: normLevel(params.message.level),
|
|
44
|
+
text: params.message.text,
|
|
45
|
+
url: params.message.url ?? '',
|
|
46
|
+
lineNumber: params.message.line ?? 0,
|
|
47
|
+
timestamp: Date.now()
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Runtime exceptions
|
|
52
|
+
client.Runtime.exceptionThrown((params) => {
|
|
53
|
+
const ex = params.exceptionDetails;
|
|
54
|
+
const message = ex.exception?.description ?? ex.text ?? 'Unknown exception';
|
|
55
|
+
const frames = ex.stackTrace?.callFrames?.map((f) => ({
|
|
56
|
+
url: f.url,
|
|
57
|
+
functionName: f.functionName,
|
|
58
|
+
lineNumber: f.lineNumber,
|
|
59
|
+
columnNumber: f.columnNumber
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const topFrame = frames?.[0];
|
|
63
|
+
opts.emit('console_message', {
|
|
64
|
+
level: 'error',
|
|
65
|
+
text: message,
|
|
66
|
+
url: ex.url ?? topFrame?.url ?? '',
|
|
67
|
+
lineNumber: ex.lineNumber ?? topFrame?.lineNumber ?? 0,
|
|
68
|
+
columnNumber: ex.columnNumber ?? topFrame?.columnNumber,
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
stackFrames: frames,
|
|
71
|
+
isException: true
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Network events
|
|
76
|
+
client.Network.requestWillBeSent((params) => {
|
|
77
|
+
opts.pendingNetworkRequests.add(params.requestId);
|
|
78
|
+
opts.emit('network_request', {
|
|
79
|
+
requestId: params.requestId,
|
|
80
|
+
url: params.request.url,
|
|
81
|
+
method: params.request.method,
|
|
82
|
+
timestamp: Date.now()
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
client.Network.responseReceived((params) => {
|
|
87
|
+
opts.pendingNetworkRequests.delete(params.requestId);
|
|
88
|
+
opts.checkNetworkIdle();
|
|
89
|
+
opts.emit('network_response', {
|
|
90
|
+
requestId: params.requestId,
|
|
91
|
+
url: params.response.url,
|
|
92
|
+
status: params.response.status,
|
|
93
|
+
mimeType: params.response.mimeType,
|
|
94
|
+
fromCache: params.response.fromDiskCache ?? false,
|
|
95
|
+
timestamp: Date.now()
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
client.Network.loadingFailed((params) => {
|
|
100
|
+
opts.pendingNetworkRequests.delete(params.requestId);
|
|
101
|
+
opts.checkNetworkIdle();
|
|
102
|
+
opts.emit('network_failed', {
|
|
103
|
+
requestId: params.requestId,
|
|
104
|
+
url: '',
|
|
105
|
+
errorText: params.errorText,
|
|
106
|
+
timestamp: Date.now()
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
client.Network.loadingFinished((params) => {
|
|
111
|
+
opts.pendingNetworkRequests.delete(params.requestId);
|
|
112
|
+
opts.checkNetworkIdle();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Page navigation
|
|
116
|
+
client.Page.frameNavigated((params) => {
|
|
117
|
+
if (params.frame.parentId === undefined) {
|
|
118
|
+
opts.onFrameNavigated(params.frame.url);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Re-export all public types and classes from the browser module
|
|
2
|
+
export { BrowserManager } from './browser-manager.js';
|
|
3
|
+
export type {
|
|
4
|
+
BrowserEventCallback,
|
|
5
|
+
TabInfo,
|
|
6
|
+
BrowserState,
|
|
7
|
+
LaunchOptions,
|
|
8
|
+
SandboxDecision
|
|
9
|
+
} from './types.js';
|
|
10
|
+
export {
|
|
11
|
+
PROFILE_NAME_RE,
|
|
12
|
+
sandboxDecision,
|
|
13
|
+
shouldDisableSandbox,
|
|
14
|
+
resolveUserDataDir,
|
|
15
|
+
extractLockPid,
|
|
16
|
+
isProcessAlive,
|
|
17
|
+
isProfileLocked,
|
|
18
|
+
findChromeExecutable,
|
|
19
|
+
findFreePort
|
|
20
|
+
} from './chrome-launcher.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { StaleElementError } from './types.js';
|
|
2
|
+
|
|
3
|
+
interface RefEntry {
|
|
4
|
+
backendNodeId: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ElementRefMap {
|
|
8
|
+
private generation = 0;
|
|
9
|
+
private refs = new Map<string, RefEntry>();
|
|
10
|
+
private counter = 0;
|
|
11
|
+
|
|
12
|
+
getGeneration(): number {
|
|
13
|
+
return this.generation;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clear refs for a new snapshot within the same document.
|
|
18
|
+
* Does not increment generation.
|
|
19
|
+
*/
|
|
20
|
+
resetRefs(): void {
|
|
21
|
+
this.refs.clear();
|
|
22
|
+
this.counter = 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Invalidate all refs due to navigation or DOM replacement.
|
|
27
|
+
* Increments generation so stale refs are detected.
|
|
28
|
+
*/
|
|
29
|
+
invalidate(): void {
|
|
30
|
+
this.refs.clear();
|
|
31
|
+
this.counter = 0;
|
|
32
|
+
this.generation++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
nextRef(): string {
|
|
36
|
+
this.counter++;
|
|
37
|
+
return `e${this.counter}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set(ref: string, backendNodeId: number): void {
|
|
41
|
+
this.refs.set(ref, { backendNodeId });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resolve(ref: string): number {
|
|
45
|
+
const entry = this.refs.get(ref);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
throw this.staleError(ref);
|
|
48
|
+
}
|
|
49
|
+
return entry.backendNodeId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
has(ref: string): boolean {
|
|
53
|
+
return this.refs.has(ref);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
size(): number {
|
|
57
|
+
return this.refs.size;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private staleError(ref: string): StaleElementError & Error {
|
|
61
|
+
const err = new Error(
|
|
62
|
+
`Element ref "${ref}" is stale. The page has navigated or the DOM has been replaced. Take a new snapshot to get fresh refs.`
|
|
63
|
+
) as StaleElementError & Error;
|
|
64
|
+
err.code = 'STALE_ELEMENT_REFERENCE';
|
|
65
|
+
err.ref = ref;
|
|
66
|
+
return err;
|
|
67
|
+
}
|
|
68
|
+
}
|