@everydaydevopsio/pilot 0.5.2 → 0.6.1
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/.mcp.json +9 -0
- package/AGENTS.md +9 -0
- package/CHANGELOG.md +64 -0
- package/CLAUDE.md +7 -7
- package/README.md +282 -69
- 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 +355 -0
- package/dist/browser/browser-manager.js.map +1 -0
- package/dist/browser/chrome-launcher.d.ts +31 -0
- package/dist/browser/chrome-launcher.d.ts.map +1 -0
- package/dist/browser/chrome-launcher.js +225 -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/preflight.d.ts +20 -0
- package/dist/browser/preflight.d.ts.map +1 -0
- package/dist/browser/preflight.js +105 -0
- package/dist/browser/preflight.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 +32 -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/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +33 -48
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/linux-check.d.ts +2 -0
- package/dist/cli/linux-check.d.ts.map +1 -0
- package/dist/cli/linux-check.js +20 -0
- package/dist/cli/linux-check.js.map +1 -0
- package/dist/cli/skill-template.d.ts +1 -1
- package/dist/cli/skill-template.d.ts.map +1 -1
- package/dist/cli/skill-template.js +3 -113
- package/dist/cli/skill-template.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/index.js +20 -2
- package/dist/mcp/index.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 +109 -11
- 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 +5 -4
- package/skills/pilot/SKILL.md +48 -0
- package/src/browser/browser-manager.ts +465 -0
- package/src/browser/chrome-launcher.ts +282 -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/preflight.ts +144 -0
- package/src/browser/security/origins.ts +102 -0
- package/src/browser/tabs.ts +110 -0
- package/src/browser/types.ts +32 -0
- package/src/browser.ts +22 -798
- package/src/cli/init.ts +40 -49
- package/src/cli/linux-check.ts +23 -0
- package/src/cli/skill-template.ts +4 -113
- 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/index.ts +24 -2
- package/src/mcp/server.ts +74 -2
- package/src/mcp/tools/browser.ts +125 -9
- 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
package/src/browser.ts
CHANGED
|
@@ -1,798 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from './
|
|
15
|
-
|
|
16
|
-
export type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface BrowserState {
|
|
26
|
-
client: Client | null;
|
|
27
|
-
targetId: string | null;
|
|
28
|
-
url: string | null;
|
|
29
|
-
connected: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface LaunchOptions {
|
|
33
|
-
headless?: boolean;
|
|
34
|
-
chromePath?: string;
|
|
35
|
-
profileName?: string;
|
|
36
|
-
viewport?: string;
|
|
37
|
-
viewportWidth?: number;
|
|
38
|
-
viewportHeight?: number;
|
|
39
|
-
deviceScaleFactor?: number;
|
|
40
|
-
responsive?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type SandboxDecision =
|
|
44
|
-
{ disable: false } | { disable: true; reason: 'env_override' | 'root_user' };
|
|
45
|
-
|
|
46
|
-
export function sandboxDecision(): SandboxDecision {
|
|
47
|
-
const envOverride = process.env.PILOT_CHROME_NO_SANDBOX;
|
|
48
|
-
if (envOverride !== undefined) {
|
|
49
|
-
const truthy = envOverride === 'true' || envOverride === '1';
|
|
50
|
-
return truthy
|
|
51
|
-
? { disable: true, reason: 'env_override' }
|
|
52
|
-
: { disable: false };
|
|
53
|
-
}
|
|
54
|
-
// --no-sandbox is relevant on Linux and (in theory) Windows.
|
|
55
|
-
// macOS uses a different sandboxing model and Chrome ignores the flag.
|
|
56
|
-
if (process.platform !== 'linux' && process.platform !== 'win32') {
|
|
57
|
-
return { disable: false };
|
|
58
|
-
}
|
|
59
|
-
// Auto-apply only when running as root. Chrome's user-namespace sandbox
|
|
60
|
-
// typically fails as root in containers, and root is the most reliable
|
|
61
|
-
// signal that the sandbox cannot work. Non-root processes (including
|
|
62
|
-
// non-root in containers) usually have a working sandbox — set
|
|
63
|
-
// PILOT_CHROME_NO_SANDBOX=true to opt in if a specific environment
|
|
64
|
-
// genuinely requires it.
|
|
65
|
-
if (process.getuid?.() === 0) {
|
|
66
|
-
return { disable: true, reason: 'root_user' };
|
|
67
|
-
}
|
|
68
|
-
return { disable: false };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function shouldDisableSandbox(): boolean {
|
|
72
|
-
return sandboxDecision().disable;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const PROFILE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
76
|
-
|
|
77
|
-
export function resolveUserDataDir(profileName: string): string {
|
|
78
|
-
if (!PROFILE_NAME_RE.test(profileName)) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Invalid profile name "${profileName}". Use only letters, digits, hyphens, and underscores.`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
const xdgData =
|
|
84
|
-
process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
85
|
-
return join(xdgData, 'pilot', profileName);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function fileOrSymlinkExists(filePath: string): boolean {
|
|
89
|
-
try {
|
|
90
|
-
lstatSync(filePath);
|
|
91
|
-
return true;
|
|
92
|
-
} catch (err: unknown) {
|
|
93
|
-
const code = (err as NodeJS.ErrnoException)?.code;
|
|
94
|
-
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
// Permission errors (EACCES) or other unexpected failures — treat as
|
|
98
|
-
// "exists" so callers don't silently ignore an inaccessible lock file.
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Extract the PID from a Chrome SingletonLock symlink target.
|
|
105
|
-
* On Linux, Chrome writes the symlink target as "hostname-pid".
|
|
106
|
-
* Returns the PID if extraction succeeds, or null otherwise.
|
|
107
|
-
*/
|
|
108
|
-
export function extractLockPid(
|
|
109
|
-
lockPath: string
|
|
110
|
-
): { pid: number; lockHost: string } | null {
|
|
111
|
-
try {
|
|
112
|
-
const stat = lstatSync(lockPath);
|
|
113
|
-
if (!stat.isSymbolicLink()) return null;
|
|
114
|
-
const target = readlinkSync(lockPath);
|
|
115
|
-
// Format: "hostname-pid" — find the last hyphen to split
|
|
116
|
-
const lastDash = target.lastIndexOf('-');
|
|
117
|
-
if (lastDash === -1) return null;
|
|
118
|
-
const lockHost = target.slice(0, lastDash);
|
|
119
|
-
const pid = parseInt(target.slice(lastDash + 1), 10);
|
|
120
|
-
if (isNaN(pid) || pid <= 0) return null;
|
|
121
|
-
return { pid, lockHost };
|
|
122
|
-
} catch {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check whether a process with the given PID is still running.
|
|
129
|
-
* Returns true if the process exists (even if we lack permission to signal it).
|
|
130
|
-
* Returns false only when the OS confirms the process does not exist (ESRCH).
|
|
131
|
-
*/
|
|
132
|
-
export function isProcessAlive(pid: number): boolean {
|
|
133
|
-
if (pid <= 0) return false;
|
|
134
|
-
try {
|
|
135
|
-
process.kill(pid, 0);
|
|
136
|
-
return true;
|
|
137
|
-
} catch (err: unknown) {
|
|
138
|
-
const code = (err as NodeJS.ErrnoException)?.code;
|
|
139
|
-
// ESRCH = no such process — the process is genuinely dead.
|
|
140
|
-
// EPERM = process exists but we lack permission to signal it — treat as alive.
|
|
141
|
-
return code !== 'ESRCH';
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Remove stale lock files from a Chrome user data directory.
|
|
147
|
-
* Only ignores ENOENT (file already gone); throws on other errors
|
|
148
|
-
* (EACCES, EPERM, etc.) so callers can fall back to treating the
|
|
149
|
-
* profile as locked.
|
|
150
|
-
*/
|
|
151
|
-
function removeStaleLocks(userDataDir: string): void {
|
|
152
|
-
for (const lockName of ['SingletonLock', 'lockfile']) {
|
|
153
|
-
try {
|
|
154
|
-
unlinkSync(join(userDataDir, lockName));
|
|
155
|
-
} catch (err: unknown) {
|
|
156
|
-
const code = (err as NodeJS.ErrnoException)?.code;
|
|
157
|
-
if (code !== 'ENOENT') throw err;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const logger = getLogger();
|
|
161
|
-
logger.warn(
|
|
162
|
-
{ userDataDir },
|
|
163
|
-
'Cleaned up stale Chrome lock files from a previous crash'
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function isProfileLocked(userDataDir: string): boolean {
|
|
168
|
-
const singletonPath = join(userDataDir, 'SingletonLock');
|
|
169
|
-
const lockfilePath = join(userDataDir, 'lockfile');
|
|
170
|
-
|
|
171
|
-
const hasSingleton = fileOrSymlinkExists(singletonPath);
|
|
172
|
-
const hasLockfile = existsSync(lockfilePath);
|
|
173
|
-
|
|
174
|
-
if (!hasSingleton && !hasLockfile) return false;
|
|
175
|
-
|
|
176
|
-
// Try to extract PID from SingletonLock symlink (Linux)
|
|
177
|
-
const lockInfo = hasSingleton ? extractLockPid(singletonPath) : null;
|
|
178
|
-
|
|
179
|
-
if (!lockInfo) {
|
|
180
|
-
// Cannot extract PID (macOS regular file, missing symlink, etc.)
|
|
181
|
-
// Fall back to conservative behavior: treat as locked.
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// If the lock was created on a different host, treat as locked
|
|
186
|
-
// (e.g. shared filesystem across machines).
|
|
187
|
-
const currentHost = hostname();
|
|
188
|
-
if (lockInfo.lockHost !== currentHost) {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Check if the process that created the lock is still running
|
|
193
|
-
if (isProcessAlive(lockInfo.pid)) {
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Process is dead — stale lock from a crash. Clean up and allow reuse.
|
|
198
|
-
// If cleanup fails (e.g. permission error), treat as locked to be safe.
|
|
199
|
-
try {
|
|
200
|
-
removeStaleLocks(userDataDir);
|
|
201
|
-
return false;
|
|
202
|
-
} catch {
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export class BrowserManager {
|
|
208
|
-
private client: Client | null = null;
|
|
209
|
-
private targetId: string | null = null;
|
|
210
|
-
private currentUrl: string | null = null;
|
|
211
|
-
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
212
|
-
private retryMs: number;
|
|
213
|
-
private destroyed = false;
|
|
214
|
-
private intentionalClose = false;
|
|
215
|
-
private eventCallback: BrowserEventCallback | null = null;
|
|
216
|
-
private pendingNetworkRequests = new Set<string>();
|
|
217
|
-
private networkIdleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
218
|
-
private networkIdleResolvers: Array<() => void> = [];
|
|
219
|
-
private chromeProcess: ChildProcess | null = null;
|
|
220
|
-
private launchedCdpPort: number | null = null;
|
|
221
|
-
private viewportConfig: ViewportConfig | null = null;
|
|
222
|
-
private launchedUserDataDir: string | null = null;
|
|
223
|
-
|
|
224
|
-
constructor(private readonly config: Config) {
|
|
225
|
-
this.retryMs = config.cdpRetryMs;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
setEventCallback(cb: BrowserEventCallback): void {
|
|
229
|
-
this.eventCallback = cb;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private emit(event: string, data: unknown): void {
|
|
233
|
-
this.eventCallback?.(event, data);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
getState(): BrowserState {
|
|
237
|
-
return {
|
|
238
|
-
client: this.client,
|
|
239
|
-
targetId: this.targetId,
|
|
240
|
-
url: this.currentUrl,
|
|
241
|
-
connected: this.client !== null
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
getClient(): Client | null {
|
|
246
|
-
return this.client;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
isConnected(): boolean {
|
|
250
|
-
return this.client !== null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
isLaunched(): boolean {
|
|
254
|
-
return this.chromeProcess !== null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
getViewportConfig(): ViewportConfig | null {
|
|
258
|
-
return this.viewportConfig;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
setViewportConfig(config: ViewportConfig): void {
|
|
262
|
-
this.viewportConfig = config;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
static findChromeExecutable(override?: string): string {
|
|
266
|
-
if (override) return override;
|
|
267
|
-
const env = process.env.PILOT_CHROME_PATH;
|
|
268
|
-
if (env) return env;
|
|
269
|
-
|
|
270
|
-
const platform = process.platform;
|
|
271
|
-
const candidates: string[] =
|
|
272
|
-
platform === 'darwin'
|
|
273
|
-
? [
|
|
274
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
275
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
276
|
-
]
|
|
277
|
-
: platform === 'linux'
|
|
278
|
-
? [
|
|
279
|
-
'/usr/bin/google-chrome-stable',
|
|
280
|
-
'/usr/bin/google-chrome',
|
|
281
|
-
'/usr/bin/chromium-browser',
|
|
282
|
-
'/usr/bin/chromium',
|
|
283
|
-
'/snap/bin/chromium'
|
|
284
|
-
]
|
|
285
|
-
: platform === 'win32'
|
|
286
|
-
? [
|
|
287
|
-
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
288
|
-
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
|
|
289
|
-
]
|
|
290
|
-
: [];
|
|
291
|
-
|
|
292
|
-
for (const p of candidates) {
|
|
293
|
-
if (existsSync(p)) return p;
|
|
294
|
-
}
|
|
295
|
-
throw new Error(
|
|
296
|
-
`Chrome not found on ${platform}. Install Google Chrome or set PILOT_CHROME_PATH.`
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
static async findFreePort(): Promise<number> {
|
|
301
|
-
return new Promise((resolve, reject) => {
|
|
302
|
-
const server = net.createServer();
|
|
303
|
-
server.listen(0, '127.0.0.1', () => {
|
|
304
|
-
const { port } = server.address() as net.AddressInfo;
|
|
305
|
-
server.close(() => resolve(port));
|
|
306
|
-
});
|
|
307
|
-
server.on('error', reject);
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async launch(
|
|
312
|
-
opts: LaunchOptions = {}
|
|
313
|
-
): Promise<{ headless: boolean; viewport: string }> {
|
|
314
|
-
if (this.chromeProcess) {
|
|
315
|
-
throw new Error('Chrome is already launched');
|
|
316
|
-
}
|
|
317
|
-
if (this.destroyed) {
|
|
318
|
-
throw new Error(
|
|
319
|
-
'BrowserManager has been destroyed; create a new instance'
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const logger = getLogger();
|
|
324
|
-
const port = await BrowserManager.findFreePort();
|
|
325
|
-
const chromePath = BrowserManager.findChromeExecutable(opts.chromePath);
|
|
326
|
-
|
|
327
|
-
const profileName = opts.profileName ?? this.config.profileName;
|
|
328
|
-
const userDataDir = resolveUserDataDir(profileName);
|
|
329
|
-
|
|
330
|
-
if (isProfileLocked(userDataDir)) {
|
|
331
|
-
throw new Error(
|
|
332
|
-
`Profile "${profileName}" is already in use by another browser instance.`
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
mkdirSync(userDataDir, { recursive: true });
|
|
337
|
-
|
|
338
|
-
const viewportPreset = opts.viewport ?? this.config.viewport;
|
|
339
|
-
this.viewportConfig = resolveViewport({
|
|
340
|
-
preset: viewportPreset,
|
|
341
|
-
width: opts.viewportWidth,
|
|
342
|
-
height: opts.viewportHeight,
|
|
343
|
-
deviceScaleFactor: opts.deviceScaleFactor,
|
|
344
|
-
responsive: opts.responsive ?? this.config.responsive
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const args = [
|
|
348
|
-
`--remote-debugging-port=${port}`,
|
|
349
|
-
`--user-data-dir=${userDataDir}`,
|
|
350
|
-
`--window-size=${this.viewportConfig.width},${this.viewportConfig.height}`,
|
|
351
|
-
'--no-first-run',
|
|
352
|
-
'--no-default-browser-check',
|
|
353
|
-
'--disable-default-apps',
|
|
354
|
-
'--disable-extensions'
|
|
355
|
-
];
|
|
356
|
-
|
|
357
|
-
const headless = opts.headless ?? this.config.headless;
|
|
358
|
-
if (headless) {
|
|
359
|
-
args.push('--headless', '--disable-gpu');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (process.platform === 'linux') {
|
|
363
|
-
args.push(
|
|
364
|
-
'--disable-dev-shm-usage',
|
|
365
|
-
'--disable-software-rasterizer',
|
|
366
|
-
'--disable-breakpad',
|
|
367
|
-
'--password-store=basic',
|
|
368
|
-
'--use-mock-keychain'
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const sandbox = sandboxDecision();
|
|
373
|
-
if (sandbox.disable) {
|
|
374
|
-
args.push('--no-sandbox');
|
|
375
|
-
logger.warn(
|
|
376
|
-
{ reason: sandbox.reason },
|
|
377
|
-
'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.'
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
logger.info(
|
|
382
|
-
{
|
|
383
|
-
chromePath,
|
|
384
|
-
port,
|
|
385
|
-
headless,
|
|
386
|
-
viewport: viewportPreset,
|
|
387
|
-
sandbox: sandbox.disable ? 'disabled' : 'enabled'
|
|
388
|
-
},
|
|
389
|
-
'Launching Chrome'
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
this.chromeProcess = spawn(chromePath, args, {
|
|
393
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
394
|
-
detached: false
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
this.launchedCdpPort = port;
|
|
398
|
-
this.launchedUserDataDir = userDataDir;
|
|
399
|
-
|
|
400
|
-
const stderrChunks: string[] = [];
|
|
401
|
-
this.chromeProcess.stderr?.on('data', (data: Buffer) => {
|
|
402
|
-
stderrChunks.push(data.toString());
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
this.chromeProcess.on('exit', (code, signal) => {
|
|
406
|
-
const stderr = stderrChunks.join('').trim();
|
|
407
|
-
const stderrFiltered = stderr
|
|
408
|
-
.split('\n')
|
|
409
|
-
.filter((l) => !l.includes('dbus/bus.cc'))
|
|
410
|
-
.join('\n')
|
|
411
|
-
.trim();
|
|
412
|
-
logger.warn(
|
|
413
|
-
{
|
|
414
|
-
code,
|
|
415
|
-
signal,
|
|
416
|
-
...(stderrFiltered && { stderr: stderrFiltered.slice(0, 1000) }),
|
|
417
|
-
...(stderr && !stderrFiltered && { stderrRaw: stderr.slice(0, 500) })
|
|
418
|
-
},
|
|
419
|
-
'Chrome process exited'
|
|
420
|
-
);
|
|
421
|
-
this.chromeProcess = null;
|
|
422
|
-
this.launchedCdpPort = null;
|
|
423
|
-
this.cleanupUserDataDir();
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
await this.waitForChromeReady(port);
|
|
427
|
-
await this.connect();
|
|
428
|
-
|
|
429
|
-
return { headless, viewport: viewportPreset };
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
private async waitForChromeReady(
|
|
433
|
-
port: number,
|
|
434
|
-
timeoutMs = 10000
|
|
435
|
-
): Promise<void> {
|
|
436
|
-
const deadline = Date.now() + timeoutMs;
|
|
437
|
-
while (Date.now() < deadline) {
|
|
438
|
-
try {
|
|
439
|
-
await CDP.List({ host: '127.0.0.1', port });
|
|
440
|
-
return;
|
|
441
|
-
} catch {
|
|
442
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
throw new Error(
|
|
446
|
-
`Chrome did not become ready on port ${port} within ${timeoutMs}ms`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async connect(): Promise<void> {
|
|
451
|
-
if (this.destroyed) return;
|
|
452
|
-
const logger = getLogger();
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
const cdpHost = this.launchedCdpPort ? '127.0.0.1' : this.config.cdpHost;
|
|
456
|
-
const cdpPort = this.launchedCdpPort ?? this.config.cdpPort;
|
|
457
|
-
|
|
458
|
-
const targets = await CDP.List({ host: cdpHost, port: cdpPort });
|
|
459
|
-
|
|
460
|
-
const pageTarget = targets.find((t) => t.type === 'page');
|
|
461
|
-
if (!pageTarget) {
|
|
462
|
-
throw new Error('No page target found in Chrome');
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const client = await CDP({
|
|
466
|
-
host: cdpHost,
|
|
467
|
-
port: cdpPort,
|
|
468
|
-
target: pageTarget.id
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
this.client = client;
|
|
472
|
-
this.targetId = pageTarget.id;
|
|
473
|
-
this.currentUrl = pageTarget.url;
|
|
474
|
-
this.retryMs = this.config.cdpRetryMs;
|
|
475
|
-
|
|
476
|
-
await this.enableDomains(client);
|
|
477
|
-
this.attachEventListeners(client);
|
|
478
|
-
|
|
479
|
-
logger.info(
|
|
480
|
-
{ targetId: this.targetId, url: this.currentUrl },
|
|
481
|
-
'Connected to Chrome'
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
this.emit('browser_connected', {
|
|
485
|
-
targetId: this.targetId,
|
|
486
|
-
url: this.currentUrl
|
|
487
|
-
});
|
|
488
|
-
} catch (err) {
|
|
489
|
-
const logger = getLogger();
|
|
490
|
-
logger.warn(
|
|
491
|
-
{ err, retryMs: this.retryMs },
|
|
492
|
-
'Failed to connect to Chrome, retrying...'
|
|
493
|
-
);
|
|
494
|
-
this.scheduleReconnect();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
private async enableDomains(client: Client): Promise<void> {
|
|
499
|
-
await Promise.all([
|
|
500
|
-
client.Network.enable({}),
|
|
501
|
-
client.Console.enable(),
|
|
502
|
-
client.Page.enable(),
|
|
503
|
-
client.Runtime.enable()
|
|
504
|
-
]);
|
|
505
|
-
|
|
506
|
-
if (this.viewportConfig) {
|
|
507
|
-
await applyViewport(client, this.viewportConfig);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private attachEventListeners(client: Client): void {
|
|
512
|
-
client.on('disconnect', () => {
|
|
513
|
-
if (this.intentionalClose) {
|
|
514
|
-
this.intentionalClose = false;
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const logger = getLogger();
|
|
518
|
-
logger.warn('Chrome disconnected');
|
|
519
|
-
this.client = null;
|
|
520
|
-
this.targetId = null;
|
|
521
|
-
this.emit('browser_disconnected', { reason: 'chrome_disconnected' });
|
|
522
|
-
this.scheduleReconnect();
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// Console events
|
|
526
|
-
// CDP uses "warning" for console.warn; normalize to "warn" to match ConsoleLevel.
|
|
527
|
-
const normLevel = (raw: string): string =>
|
|
528
|
-
raw === 'warning' ? 'warn' : raw;
|
|
529
|
-
|
|
530
|
-
client.Runtime.consoleAPICalled((params) => {
|
|
531
|
-
const text = params.args
|
|
532
|
-
.map((a) =>
|
|
533
|
-
a.value !== undefined ? String(a.value) : (a.description ?? '')
|
|
534
|
-
)
|
|
535
|
-
.join(' ');
|
|
536
|
-
this.emit('console_message', {
|
|
537
|
-
level: normLevel(params.type),
|
|
538
|
-
text,
|
|
539
|
-
url: params.stackTrace?.callFrames?.[0]?.url ?? '',
|
|
540
|
-
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber ?? 0,
|
|
541
|
-
timestamp: Date.now()
|
|
542
|
-
});
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
client.Console.messageAdded((params) => {
|
|
546
|
-
this.emit('console_message', {
|
|
547
|
-
level: normLevel(params.message.level),
|
|
548
|
-
text: params.message.text,
|
|
549
|
-
url: params.message.url ?? '',
|
|
550
|
-
lineNumber: params.message.line ?? 0,
|
|
551
|
-
timestamp: Date.now()
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
// Network events
|
|
556
|
-
client.Network.requestWillBeSent((params) => {
|
|
557
|
-
this.pendingNetworkRequests.add(params.requestId);
|
|
558
|
-
this.emit('network_request', {
|
|
559
|
-
requestId: params.requestId,
|
|
560
|
-
url: params.request.url,
|
|
561
|
-
method: params.request.method,
|
|
562
|
-
timestamp: Date.now()
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
client.Network.responseReceived((params) => {
|
|
567
|
-
this.pendingNetworkRequests.delete(params.requestId);
|
|
568
|
-
this.checkNetworkIdle();
|
|
569
|
-
this.emit('network_response', {
|
|
570
|
-
requestId: params.requestId,
|
|
571
|
-
url: params.response.url,
|
|
572
|
-
status: params.response.status,
|
|
573
|
-
mimeType: params.response.mimeType,
|
|
574
|
-
fromCache: params.response.fromDiskCache ?? false,
|
|
575
|
-
timestamp: Date.now()
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
client.Network.loadingFailed((params) => {
|
|
580
|
-
this.pendingNetworkRequests.delete(params.requestId);
|
|
581
|
-
this.checkNetworkIdle();
|
|
582
|
-
this.emit('network_failed', {
|
|
583
|
-
requestId: params.requestId,
|
|
584
|
-
url: '',
|
|
585
|
-
errorText: params.errorText,
|
|
586
|
-
timestamp: Date.now()
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
client.Network.loadingFinished((params) => {
|
|
591
|
-
this.pendingNetworkRequests.delete(params.requestId);
|
|
592
|
-
this.checkNetworkIdle();
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// Page navigation
|
|
596
|
-
client.Page.frameNavigated((params) => {
|
|
597
|
-
if (params.frame.parentId === undefined) {
|
|
598
|
-
this.currentUrl = params.frame.url;
|
|
599
|
-
this.emit('page_navigated', {
|
|
600
|
-
url: params.frame.url,
|
|
601
|
-
timestamp: Date.now()
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
private checkNetworkIdle(): void {
|
|
608
|
-
if (this.pendingNetworkRequests.size === 0) {
|
|
609
|
-
if (this.networkIdleTimer) clearTimeout(this.networkIdleTimer);
|
|
610
|
-
this.networkIdleTimer = setTimeout(() => {
|
|
611
|
-
const resolvers = this.networkIdleResolvers.splice(0);
|
|
612
|
-
resolvers.forEach((r) => r());
|
|
613
|
-
}, 500);
|
|
614
|
-
this.networkIdleTimer.unref();
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
waitForNetworkIdle(timeoutMs = 10000): Promise<void> {
|
|
619
|
-
return new Promise((resolve, reject) => {
|
|
620
|
-
if (this.pendingNetworkRequests.size === 0) {
|
|
621
|
-
resolve();
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
const timer = setTimeout(() => {
|
|
625
|
-
const idx = this.networkIdleResolvers.indexOf(resolve);
|
|
626
|
-
if (idx !== -1) this.networkIdleResolvers.splice(idx, 1);
|
|
627
|
-
reject(new Error('timeout waiting for network idle'));
|
|
628
|
-
}, timeoutMs);
|
|
629
|
-
|
|
630
|
-
this.networkIdleResolvers.push(() => {
|
|
631
|
-
clearTimeout(timer);
|
|
632
|
-
resolve();
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
private getCdpConnectionInfo(): { host: string; port: number } {
|
|
638
|
-
return {
|
|
639
|
-
host: this.launchedCdpPort ? '127.0.0.1' : this.config.cdpHost,
|
|
640
|
-
port: this.launchedCdpPort ?? this.config.cdpPort
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async listTabs(): Promise<TabInfo[]> {
|
|
645
|
-
const { host, port } = this.getCdpConnectionInfo();
|
|
646
|
-
const targets = await CDP.List({ host, port });
|
|
647
|
-
return targets
|
|
648
|
-
.filter((t) => t.type === 'page')
|
|
649
|
-
.map((t) => ({
|
|
650
|
-
targetId: t.id,
|
|
651
|
-
url: t.url,
|
|
652
|
-
title: t.title,
|
|
653
|
-
active: t.id === this.targetId
|
|
654
|
-
}));
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async createTab(url?: string): Promise<TabInfo> {
|
|
658
|
-
const { host, port } = this.getCdpConnectionInfo();
|
|
659
|
-
// chrome-remote-interface appends the url to the HTTP request path
|
|
660
|
-
// without encoding, so we must encode it ourselves to avoid
|
|
661
|
-
// ERR_UNESCAPED_CHARACTERS on URLs with special characters.
|
|
662
|
-
const encodedUrl = url ? encodeURI(url) : undefined;
|
|
663
|
-
const target = await CDP.New({ host, port, url: encodedUrl });
|
|
664
|
-
return {
|
|
665
|
-
targetId: target.id,
|
|
666
|
-
url: target.url,
|
|
667
|
-
title: target.title,
|
|
668
|
-
active: false
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
async closeTab(targetId: string): Promise<void> {
|
|
673
|
-
if (targetId === this.targetId) {
|
|
674
|
-
throw new Error(
|
|
675
|
-
'Cannot close the active tab. Switch to another tab first.'
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
const { host, port } = this.getCdpConnectionInfo();
|
|
679
|
-
await CDP.Close({ host, port, id: targetId });
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async switchTab(targetId: string): Promise<void> {
|
|
683
|
-
if (targetId === this.targetId) return;
|
|
684
|
-
|
|
685
|
-
const { host, port } = this.getCdpConnectionInfo();
|
|
686
|
-
|
|
687
|
-
// Verify the target exists
|
|
688
|
-
const targets = await CDP.List({ host, port });
|
|
689
|
-
const target = targets.find((t) => t.id === targetId && t.type === 'page');
|
|
690
|
-
if (!target) {
|
|
691
|
-
throw new Error(`No page tab found with targetId: ${targetId}`);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Close old client connection (but not the tab)
|
|
695
|
-
if (this.client) {
|
|
696
|
-
this.intentionalClose = true;
|
|
697
|
-
await this.client.close();
|
|
698
|
-
this.client = null;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Clear any pending reconnect from a prior disconnect
|
|
702
|
-
if (this.retryTimer) {
|
|
703
|
-
clearTimeout(this.retryTimer);
|
|
704
|
-
this.retryTimer = null;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Connect to the new target
|
|
708
|
-
const client = await CDP({ host, port, target: targetId });
|
|
709
|
-
this.client = client;
|
|
710
|
-
this.targetId = targetId;
|
|
711
|
-
this.currentUrl = target.url;
|
|
712
|
-
this.retryMs = this.config.cdpRetryMs;
|
|
713
|
-
|
|
714
|
-
await this.enableDomains(client);
|
|
715
|
-
this.attachEventListeners(client);
|
|
716
|
-
|
|
717
|
-
// Activate the tab in the browser UI
|
|
718
|
-
await client.Target.activateTarget({ targetId });
|
|
719
|
-
|
|
720
|
-
const logger = getLogger();
|
|
721
|
-
logger.info({ targetId, url: target.url }, 'Switched to tab');
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
private scheduleReconnect(): void {
|
|
725
|
-
if (this.destroyed) return;
|
|
726
|
-
if (this.retryTimer) return;
|
|
727
|
-
|
|
728
|
-
this.retryTimer = setTimeout(() => {
|
|
729
|
-
this.retryTimer = null;
|
|
730
|
-
this.retryMs = Math.min(this.retryMs * 2, this.config.cdpMaxRetryMs);
|
|
731
|
-
void this.connect();
|
|
732
|
-
}, this.retryMs);
|
|
733
|
-
this.retryTimer.unref();
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
private cleanupUserDataDir(): void {
|
|
737
|
-
if (this.launchedUserDataDir) {
|
|
738
|
-
// Remove stale lock files left behind after Chrome exits so the
|
|
739
|
-
// profile can be reused without a false "already in use" error.
|
|
740
|
-
for (const lockName of ['SingletonLock', 'lockfile']) {
|
|
741
|
-
try {
|
|
742
|
-
unlinkSync(join(this.launchedUserDataDir, lockName));
|
|
743
|
-
} catch {
|
|
744
|
-
// lock file may not exist — that's fine
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
this.launchedUserDataDir = null;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
async destroy(): Promise<void> {
|
|
752
|
-
this.destroyed = true;
|
|
753
|
-
if (this.retryTimer) {
|
|
754
|
-
clearTimeout(this.retryTimer);
|
|
755
|
-
this.retryTimer = null;
|
|
756
|
-
}
|
|
757
|
-
if (this.networkIdleTimer) {
|
|
758
|
-
clearTimeout(this.networkIdleTimer);
|
|
759
|
-
}
|
|
760
|
-
if (this.client) {
|
|
761
|
-
await this.client.close();
|
|
762
|
-
this.client = null;
|
|
763
|
-
}
|
|
764
|
-
if (this.chromeProcess) {
|
|
765
|
-
const proc = this.chromeProcess;
|
|
766
|
-
this.chromeProcess = null;
|
|
767
|
-
this.launchedCdpPort = null;
|
|
768
|
-
try {
|
|
769
|
-
proc.kill();
|
|
770
|
-
} catch {
|
|
771
|
-
// process may have already exited
|
|
772
|
-
}
|
|
773
|
-
// Wait for Chrome to fully exit before removing lock files so a
|
|
774
|
-
// concurrent launch cannot race against a still-running process.
|
|
775
|
-
// `proc.killed` only confirms a signal was delivered, not that the
|
|
776
|
-
// process is gone — only `exitCode !== null` or the `exit` event do.
|
|
777
|
-
await new Promise<void>((resolve) => {
|
|
778
|
-
if (proc.exitCode !== null) {
|
|
779
|
-
resolve();
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
const timer = setTimeout(() => {
|
|
783
|
-
try {
|
|
784
|
-
proc.kill('SIGKILL');
|
|
785
|
-
} catch {
|
|
786
|
-
// process may have already exited
|
|
787
|
-
}
|
|
788
|
-
resolve();
|
|
789
|
-
}, 5000);
|
|
790
|
-
proc.once('exit', () => {
|
|
791
|
-
clearTimeout(timer);
|
|
792
|
-
resolve();
|
|
793
|
-
});
|
|
794
|
-
});
|
|
795
|
-
this.cleanupUserDataDir();
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
1
|
+
// Backward-compatible re-export from the refactored browser module.
|
|
2
|
+
// All public types, classes, and functions are now defined in src/browser/.
|
|
3
|
+
export {
|
|
4
|
+
BrowserManager,
|
|
5
|
+
PROFILE_NAME_RE,
|
|
6
|
+
sandboxDecision,
|
|
7
|
+
shouldDisableSandbox,
|
|
8
|
+
resolveUserDataDir,
|
|
9
|
+
extractLockPid,
|
|
10
|
+
isProcessAlive,
|
|
11
|
+
isProfileLocked,
|
|
12
|
+
findChromeExecutable,
|
|
13
|
+
findFreePort
|
|
14
|
+
} from './browser/index.js';
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
BrowserEventCallback,
|
|
18
|
+
TabInfo,
|
|
19
|
+
BrowserState,
|
|
20
|
+
LaunchOptions,
|
|
21
|
+
SandboxDecision
|
|
22
|
+
} from './browser/index.js';
|