pi-agent-browser-native 0.2.43 → 0.2.45
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 +37 -0
- package/README.md +26 -16
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +49 -27
- package/docs/ELECTRON.md +1 -1
- package/docs/RELEASE.md +16 -9
- package/docs/REQUIREMENTS.md +6 -3
- package/docs/SUPPORT_MATRIX.md +18 -14
- package/docs/TOOL_CONTRACT.md +87 -46
- package/docs/platform-smoke.md +15 -9
- package/extensions/agent-browser/index.ts +29 -445
- package/extensions/agent-browser/lib/bash-guard.ts +205 -0
- package/extensions/agent-browser/lib/electron/cdp.ts +69 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +5 -58
- package/extensions/agent-browser/lib/electron/discovery.ts +2 -9
- package/extensions/agent-browser/lib/electron/launch.ts +11 -65
- package/extensions/agent-browser/lib/electron/text.ts +13 -0
- package/extensions/agent-browser/lib/fs-utils.ts +18 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +207 -21
- package/extensions/agent-browser/lib/input-modes/params.ts +17 -7
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +22 -2
- package/extensions/agent-browser/lib/input-modes/types.ts +5 -1
- package/extensions/agent-browser/lib/input-modes.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +82 -11
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +153 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +53 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +751 -32
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +38 -7
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -46
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +10 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +28 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +1 -6
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +15 -3
- package/extensions/agent-browser/lib/orchestration/output-file.ts +86 -0
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +231 -0
- package/extensions/agent-browser/lib/playbook.ts +26 -26
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/prompt-policy.ts +1 -18
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +1 -4
- package/extensions/agent-browser/lib/results/artifact-state.ts +7 -3
- package/extensions/agent-browser/lib/results/contracts.ts +6 -2
- package/extensions/agent-browser/lib/results/envelope.ts +11 -2
- package/extensions/agent-browser/lib/results/network-routes.ts +7 -4
- package/extensions/agent-browser/lib/results/network.ts +7 -1
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +88 -20
- package/extensions/agent-browser/lib/results/presentation/batch.ts +84 -12
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +81 -26
- package/extensions/agent-browser/lib/results/presentation/errors.ts +13 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +60 -0
- package/extensions/agent-browser/lib/results/presentation.ts +10 -1
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +16 -5
- package/extensions/agent-browser/lib/results/snapshot.ts +2 -0
- package/extensions/agent-browser/lib/runtime.ts +10 -1
- package/extensions/agent-browser/lib/session-page-state.ts +15 -6
- package/extensions/agent-browser/lib/web-search.ts +1 -1
- package/package.json +5 -5
- package/platform-smoke.config.mjs +15 -3
- package/scripts/doctor.mjs +70 -1
- package/scripts/platform-smoke/build-ubuntu-image.mjs +25 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +62 -30
- package/scripts/platform-smoke/doctor.mjs +28 -11
- package/scripts/platform-smoke/linux-image/Dockerfile +3 -5
- package/scripts/platform-smoke/targets.mjs +60 -22
- package/scripts/platform-smoke.mjs +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +0 -154
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV = "PI_AGENT_BROWSER_ALLOW_DIRECT_BASH";
|
|
5
|
+
const DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN = /^(?:[.~]|\.\.?|\/)?(?:[^\s;&|]+\/)?agent-browser$/;
|
|
6
|
+
const HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN = /^\s*(?:command\s+-v|which|type\s+-P)\s+agent-browser\s*$/;
|
|
7
|
+
const PACKAGE_NAME = "pi-agent-browser-native";
|
|
8
|
+
|
|
9
|
+
type ShellQuoteState = "double" | "single" | undefined;
|
|
10
|
+
|
|
11
|
+
function isShellAssignmentToken(token: string): boolean {
|
|
12
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stripOuterQuotes(token: string): string {
|
|
16
|
+
if (token.length >= 2 && ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'")))) {
|
|
17
|
+
return token.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
return token;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function segmentLaunchesAgentBrowser(tokens: string[]): boolean {
|
|
23
|
+
let index = 0;
|
|
24
|
+
while (index < tokens.length && isShellAssignmentToken(tokens[index])) {
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
if (index >= tokens.length) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let executableToken = tokens[index];
|
|
32
|
+
if (executableToken === "env") {
|
|
33
|
+
index += 1;
|
|
34
|
+
while (index < tokens.length && isShellAssignmentToken(tokens[index])) {
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
executableToken = tokens[index] ?? "";
|
|
38
|
+
}
|
|
39
|
+
if (executableToken === "npx" || executableToken === "bunx") {
|
|
40
|
+
index += 1;
|
|
41
|
+
while (index < tokens.length && tokens[index].startsWith("-")) {
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
executableToken = tokens[index] ?? "";
|
|
45
|
+
}
|
|
46
|
+
if (executableToken === "pnpm" || executableToken === "yarn") {
|
|
47
|
+
index += 1;
|
|
48
|
+
if (tokens[index] !== "dlx") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
index += 1;
|
|
52
|
+
while (index < tokens.length && tokens[index].startsWith("-")) {
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
executableToken = tokens[index] ?? "";
|
|
56
|
+
}
|
|
57
|
+
return DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN.test(executableToken);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Best-effort detection for common direct launches only. This is an ergonomics guard,
|
|
61
|
+
// not a general-purpose bash parser or security boundary.
|
|
62
|
+
export function looksLikeDirectAgentBrowserBash(command: string): boolean {
|
|
63
|
+
let currentToken = "";
|
|
64
|
+
let quoteState: ShellQuoteState;
|
|
65
|
+
let awaitingHeredocDelimiter: { stripTabs: boolean } | undefined;
|
|
66
|
+
let pendingHeredoc: { delimiter: string; stripTabs: boolean } | undefined;
|
|
67
|
+
let pendingHeredocLine = "";
|
|
68
|
+
let segmentTokens: string[] = [];
|
|
69
|
+
|
|
70
|
+
const acceptToken = (token: string) => {
|
|
71
|
+
if (token.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (awaitingHeredocDelimiter) {
|
|
75
|
+
pendingHeredoc = {
|
|
76
|
+
delimiter: stripOuterQuotes(token),
|
|
77
|
+
stripTabs: awaitingHeredocDelimiter.stripTabs,
|
|
78
|
+
};
|
|
79
|
+
awaitingHeredocDelimiter = undefined;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
segmentTokens.push(token);
|
|
83
|
+
};
|
|
84
|
+
const flushToken = () => {
|
|
85
|
+
acceptToken(currentToken);
|
|
86
|
+
currentToken = "";
|
|
87
|
+
};
|
|
88
|
+
const flushSegment = () => {
|
|
89
|
+
const launchesAgentBrowser = segmentLaunchesAgentBrowser(segmentTokens);
|
|
90
|
+
segmentTokens = [];
|
|
91
|
+
return launchesAgentBrowser;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
95
|
+
const char = command[index];
|
|
96
|
+
if (pendingHeredoc) {
|
|
97
|
+
if (char === "\n") {
|
|
98
|
+
const candidate = pendingHeredoc.stripTabs ? pendingHeredocLine.replace(/^\t+/, "") : pendingHeredocLine;
|
|
99
|
+
if (candidate === pendingHeredoc.delimiter) {
|
|
100
|
+
pendingHeredoc = undefined;
|
|
101
|
+
}
|
|
102
|
+
pendingHeredocLine = "";
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
pendingHeredocLine += char;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (quoteState === "single") {
|
|
110
|
+
currentToken += char;
|
|
111
|
+
if (char === "'") {
|
|
112
|
+
quoteState = undefined;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (quoteState === "double") {
|
|
117
|
+
currentToken += char;
|
|
118
|
+
if (char === "\\" && index + 1 < command.length) {
|
|
119
|
+
currentToken += command[index + 1];
|
|
120
|
+
index += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (char === '"') {
|
|
124
|
+
quoteState = undefined;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === "'" || char === '"') {
|
|
129
|
+
currentToken += char;
|
|
130
|
+
quoteState = char === "'" ? "single" : "double";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === "\\" && index + 1 < command.length) {
|
|
134
|
+
currentToken += char;
|
|
135
|
+
currentToken += command[index + 1];
|
|
136
|
+
index += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "\n") {
|
|
140
|
+
flushToken();
|
|
141
|
+
if (flushSegment()) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (/\s/.test(char)) {
|
|
147
|
+
flushToken();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const threeCharOperator = command.slice(index, index + 3);
|
|
151
|
+
if (threeCharOperator === "<<-") {
|
|
152
|
+
flushToken();
|
|
153
|
+
awaitingHeredocDelimiter = { stripTabs: true };
|
|
154
|
+
index += 2;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const twoCharOperator = command.slice(index, index + 2);
|
|
158
|
+
if (twoCharOperator === "<<") {
|
|
159
|
+
flushToken();
|
|
160
|
+
awaitingHeredocDelimiter = { stripTabs: false };
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (twoCharOperator === "&&" || twoCharOperator === "||") {
|
|
165
|
+
flushToken();
|
|
166
|
+
if (flushSegment()) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (char === "|" || char === ";" || char === "&") {
|
|
173
|
+
flushToken();
|
|
174
|
+
if (flushSegment()) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
currentToken += char;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
flushToken();
|
|
183
|
+
return flushSegment();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function isHarmlessAgentBrowserInspectionCommand(command: string): boolean {
|
|
187
|
+
return HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN.test(command);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isTruthyEnvValue(value: string | undefined): boolean {
|
|
191
|
+
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function isPackageDevelopmentCwd(cwd: string): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
const packageJson = JSON.parse(await readFile(join(cwd, "package.json"), "utf8")) as { name?: unknown };
|
|
197
|
+
return packageJson.name === PACKAGE_NAME;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function isDirectAgentBrowserBashAllowed(cwd: string): Promise<boolean> {
|
|
204
|
+
return isTruthyEnvValue(process.env[DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV]) || await isPackageDevelopmentCwd(cwd);
|
|
205
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Parse and fetch Chrome DevTools Protocol metadata for wrapper-owned Electron launches.
|
|
3
|
+
* Responsibilities: Normalize CDP version/target JSON and perform bounded localhost CDP JSON fetches.
|
|
4
|
+
* Scope: Tiny Electron CDP boundary helpers only; launch, status, cleanup, and target selection stay in their owning modules.
|
|
5
|
+
* Usage: Imported by Electron launch and cleanup paths when polling `/json/version` and `/json/list`.
|
|
6
|
+
* Invariants/Assumptions: Malformed or unavailable CDP endpoints return undefined/empty metadata rather than throwing, matching prior caller behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isRecord } from "../parsing.js";
|
|
10
|
+
|
|
11
|
+
const ELECTRON_CDP_FETCH_TIMEOUT_MS = 1_000;
|
|
12
|
+
|
|
13
|
+
export interface ElectronCdpVersion {
|
|
14
|
+
browser?: string;
|
|
15
|
+
protocolVersion?: string;
|
|
16
|
+
userAgent?: string;
|
|
17
|
+
v8Version?: string;
|
|
18
|
+
webKitVersion?: string;
|
|
19
|
+
webSocketDebuggerUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ElectronCdpTarget {
|
|
23
|
+
id?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
webSocketDebuggerUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function asString(value: unknown): string | undefined {
|
|
31
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseCdpVersion(value: unknown): ElectronCdpVersion | undefined {
|
|
35
|
+
if (!isRecord(value)) return undefined;
|
|
36
|
+
return {
|
|
37
|
+
browser: asString(value.Browser) ?? asString(value.browser),
|
|
38
|
+
protocolVersion: asString(value["Protocol-Version"]) ?? asString(value.protocolVersion),
|
|
39
|
+
userAgent: asString(value["User-Agent"]) ?? asString(value.userAgent),
|
|
40
|
+
v8Version: asString(value["V8-Version"]) ?? asString(value.v8Version),
|
|
41
|
+
webKitVersion: asString(value["WebKit-Version"]) ?? asString(value.webKitVersion),
|
|
42
|
+
webSocketDebuggerUrl: asString(value.webSocketDebuggerUrl),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseCdpTargets(value: unknown): ElectronCdpTarget[] {
|
|
47
|
+
if (!Array.isArray(value)) return [];
|
|
48
|
+
return value.filter(isRecord).map((target) => ({
|
|
49
|
+
id: asString(target.id),
|
|
50
|
+
title: asString(target.title),
|
|
51
|
+
type: asString(target.type),
|
|
52
|
+
url: asString(target.url),
|
|
53
|
+
webSocketDebuggerUrl: asString(target.webSocketDebuggerUrl),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function fetchCdpJson(url: string): Promise<unknown | undefined> {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timeout = setTimeout(() => controller.abort(), ELECTRON_CDP_FETCH_TIMEOUT_MS);
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
62
|
+
if (!response.ok) return undefined;
|
|
63
|
+
return await response.json() as unknown;
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { execFile, type ChildProcess } from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import { rm } from "node:fs/promises";
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
|
|
13
|
+
import { fetchCdpJson, parseCdpTargets, parseCdpVersion } from "./cdp.js";
|
|
13
14
|
import { ELECTRON_PROFILE_DIR_PREFIX, type ElectronCdpTarget, type ElectronCdpVersion, type ElectronLaunchRecord } from "./launch.js";
|
|
15
|
+
import { pathExists } from "../fs-utils.js";
|
|
14
16
|
import { getSecureTempChildDirectoryValidationError } from "../temp.js";
|
|
15
17
|
|
|
16
|
-
const ELECTRON_STATUS_FETCH_TIMEOUT_MS = 1_000;
|
|
17
18
|
const ELECTRON_CLEANUP_DEFAULT_TIMEOUT_MS = 5_000;
|
|
18
19
|
const ELECTRON_CLEANUP_POLL_INTERVAL_MS = 100;
|
|
19
20
|
const RESTORED_PROCESS_COMMAND_TIMEOUT_MS = 1_000;
|
|
@@ -50,51 +51,6 @@ function sleep(ms: number): Promise<void> {
|
|
|
50
51
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
|
-
return typeof value === "object" && value !== null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function asString(value: unknown): string | undefined {
|
|
58
|
-
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseCdpVersion(value: unknown): ElectronCdpVersion | undefined {
|
|
62
|
-
if (!isRecord(value)) return undefined;
|
|
63
|
-
return {
|
|
64
|
-
browser: asString(value.Browser) ?? asString(value.browser),
|
|
65
|
-
protocolVersion: asString(value["Protocol-Version"]) ?? asString(value.protocolVersion),
|
|
66
|
-
userAgent: asString(value["User-Agent"]) ?? asString(value.userAgent),
|
|
67
|
-
v8Version: asString(value["V8-Version"]) ?? asString(value.v8Version),
|
|
68
|
-
webKitVersion: asString(value["WebKit-Version"]) ?? asString(value.webKitVersion),
|
|
69
|
-
webSocketDebuggerUrl: asString(value.webSocketDebuggerUrl),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseCdpTargets(value: unknown): ElectronCdpTarget[] {
|
|
74
|
-
if (!Array.isArray(value)) return [];
|
|
75
|
-
return value.filter(isRecord).map((target) => ({
|
|
76
|
-
id: asString(target.id),
|
|
77
|
-
title: asString(target.title),
|
|
78
|
-
type: asString(target.type),
|
|
79
|
-
url: asString(target.url),
|
|
80
|
-
webSocketDebuggerUrl: asString(target.webSocketDebuggerUrl),
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function fetchJson(url: string): Promise<unknown | undefined> {
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
const timeout = setTimeout(() => controller.abort(), ELECTRON_STATUS_FETCH_TIMEOUT_MS);
|
|
87
|
-
try {
|
|
88
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
89
|
-
if (!response.ok) return undefined;
|
|
90
|
-
return await response.json() as unknown;
|
|
91
|
-
} catch {
|
|
92
|
-
return undefined;
|
|
93
|
-
} finally {
|
|
94
|
-
clearTimeout(timeout);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
54
|
function isPidAlive(pid: number | undefined): boolean | undefined {
|
|
99
55
|
if (!pid || !Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
100
56
|
try {
|
|
@@ -106,19 +62,10 @@ function isPidAlive(pid: number | undefined): boolean | undefined {
|
|
|
106
62
|
}
|
|
107
63
|
}
|
|
108
64
|
|
|
109
|
-
async function pathExists(path: string): Promise<boolean> {
|
|
110
|
-
try {
|
|
111
|
-
await access(path);
|
|
112
|
-
return true;
|
|
113
|
-
} catch {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
65
|
async function isPortAlive(port: number): Promise<{ targets: ElectronCdpTarget[]; version?: ElectronCdpVersion }> {
|
|
119
|
-
const version = parseCdpVersion(await
|
|
66
|
+
const version = parseCdpVersion(await fetchCdpJson(`http://127.0.0.1:${port}/json/version`));
|
|
120
67
|
if (!version) return { targets: [] };
|
|
121
|
-
const targets = parseCdpTargets(await
|
|
68
|
+
const targets = parseCdpTargets(await fetchCdpJson(`http://127.0.0.1:${port}/json/list`));
|
|
122
69
|
return { targets, version };
|
|
123
70
|
}
|
|
124
71
|
|
|
@@ -11,6 +11,8 @@ import { access, readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
13
13
|
|
|
14
|
+
import { pathExists } from "../fs-utils.js";
|
|
15
|
+
|
|
14
16
|
export const ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS = 50;
|
|
15
17
|
export const ELECTRON_DISCOVERY_MAX_RESULTS = 200;
|
|
16
18
|
|
|
@@ -150,15 +152,6 @@ function resolveLocations(locations: ElectronDiscoveryScanLocations | undefined)
|
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
async function pathExists(path: string): Promise<boolean> {
|
|
154
|
-
try {
|
|
155
|
-
await access(path, fsConstants.F_OK);
|
|
156
|
-
return true;
|
|
157
|
-
} catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
155
|
async function isDirectory(path: string): Promise<boolean> {
|
|
163
156
|
try {
|
|
164
157
|
return (await stat(path)).isDirectory();
|
|
@@ -11,6 +11,13 @@ import { randomUUID } from "node:crypto";
|
|
|
11
11
|
import { readFile, rm } from "node:fs/promises";
|
|
12
12
|
import { dirname } from "node:path";
|
|
13
13
|
|
|
14
|
+
import {
|
|
15
|
+
fetchCdpJson,
|
|
16
|
+
parseCdpTargets,
|
|
17
|
+
parseCdpVersion,
|
|
18
|
+
type ElectronCdpTarget,
|
|
19
|
+
type ElectronCdpVersion,
|
|
20
|
+
} from "./cdp.js";
|
|
14
21
|
import {
|
|
15
22
|
discoverElectronApps,
|
|
16
23
|
inspectElectronAppPath,
|
|
@@ -19,6 +26,8 @@ import {
|
|
|
19
26
|
} from "./discovery.js";
|
|
20
27
|
import { createSecureTempDirectory } from "../temp.js";
|
|
21
28
|
|
|
29
|
+
export type { ElectronCdpTarget, ElectronCdpVersion } from "./cdp.js";
|
|
30
|
+
|
|
22
31
|
export const ELECTRON_LAUNCH_RECORD_VERSION = 1;
|
|
23
32
|
export const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
|
|
24
33
|
export const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
|
|
@@ -27,7 +36,6 @@ const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
|
27
36
|
export const ELECTRON_PROFILE_DIR_PREFIX = "electron-profile-";
|
|
28
37
|
const ELECTRON_DEFAULT_APP_ARGS = ["--disable-extensions", "--no-first-run", "--no-default-browser-check"] as const;
|
|
29
38
|
const ELECTRON_DEVTOOLS_POLL_INTERVAL_MS = 100;
|
|
30
|
-
const ELECTRON_CDP_FETCH_TIMEOUT_MS = 1_000;
|
|
31
39
|
|
|
32
40
|
export interface ElectronDevToolsActivePortRead {
|
|
33
41
|
error?: string;
|
|
@@ -59,23 +67,6 @@ export type ElectronLaunchFailureReason =
|
|
|
59
67
|
| "spawn-error"
|
|
60
68
|
| "timeout";
|
|
61
69
|
|
|
62
|
-
export interface ElectronCdpVersion {
|
|
63
|
-
browser?: string;
|
|
64
|
-
protocolVersion?: string;
|
|
65
|
-
userAgent?: string;
|
|
66
|
-
v8Version?: string;
|
|
67
|
-
webKitVersion?: string;
|
|
68
|
-
webSocketDebuggerUrl?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ElectronCdpTarget {
|
|
72
|
-
id?: string;
|
|
73
|
-
title?: string;
|
|
74
|
-
type?: string;
|
|
75
|
-
url?: string;
|
|
76
|
-
webSocketDebuggerUrl?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
70
|
export interface ElectronLaunchRecord {
|
|
80
71
|
appName: string;
|
|
81
72
|
appPath?: string;
|
|
@@ -200,37 +191,6 @@ export async function resolveElectronLaunchTarget(options: ResolveElectronTarget
|
|
|
200
191
|
return undefined;
|
|
201
192
|
}
|
|
202
193
|
|
|
203
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
204
|
-
return typeof value === "object" && value !== null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function asString(value: unknown): string | undefined {
|
|
208
|
-
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function parseCdpVersion(value: unknown): ElectronCdpVersion | undefined {
|
|
212
|
-
if (!isRecord(value)) return undefined;
|
|
213
|
-
return {
|
|
214
|
-
browser: asString(value.Browser) ?? asString(value.browser),
|
|
215
|
-
protocolVersion: asString(value["Protocol-Version"]) ?? asString(value.protocolVersion),
|
|
216
|
-
userAgent: asString(value["User-Agent"]) ?? asString(value.userAgent),
|
|
217
|
-
v8Version: asString(value["V8-Version"]) ?? asString(value.v8Version),
|
|
218
|
-
webKitVersion: asString(value["WebKit-Version"]) ?? asString(value.webKitVersion),
|
|
219
|
-
webSocketDebuggerUrl: asString(value.webSocketDebuggerUrl),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function parseCdpTargets(value: unknown): ElectronCdpTarget[] {
|
|
224
|
-
if (!Array.isArray(value)) return [];
|
|
225
|
-
return value.filter(isRecord).map((target) => ({
|
|
226
|
-
id: asString(target.id),
|
|
227
|
-
title: asString(target.title),
|
|
228
|
-
type: asString(target.type),
|
|
229
|
-
url: asString(target.url),
|
|
230
|
-
webSocketDebuggerUrl: asString(target.webSocketDebuggerUrl),
|
|
231
|
-
}));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
194
|
function targetMatchesType(target: ElectronCdpTarget, targetType: "any" | "page" | "webview" | undefined): boolean {
|
|
235
195
|
return targetType === undefined || targetType === "any" || target.type === targetType;
|
|
236
196
|
}
|
|
@@ -245,20 +205,6 @@ function selectElectronConnectArg(options: {
|
|
|
245
205
|
return targetWebSocket ?? options.version.webSocketDebuggerUrl ?? String(options.port);
|
|
246
206
|
}
|
|
247
207
|
|
|
248
|
-
async function fetchJson(url: string): Promise<unknown | undefined> {
|
|
249
|
-
const controller = new AbortController();
|
|
250
|
-
const timeout = setTimeout(() => controller.abort(), ELECTRON_CDP_FETCH_TIMEOUT_MS);
|
|
251
|
-
try {
|
|
252
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
253
|
-
if (!response.ok) return undefined;
|
|
254
|
-
return await response.json() as unknown;
|
|
255
|
-
} catch {
|
|
256
|
-
return undefined;
|
|
257
|
-
} finally {
|
|
258
|
-
clearTimeout(timeout);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
208
|
async function readDevToolsActivePort(userDataDir: string): Promise<ElectronDevToolsActivePortRead> {
|
|
263
209
|
const path = `${userDataDir}/${DEVTOOLS_ACTIVE_PORT_FILE}`;
|
|
264
210
|
try {
|
|
@@ -304,9 +250,9 @@ async function pollDevToolsActivePort(options: {
|
|
|
304
250
|
|
|
305
251
|
async function pollCdpMetadata(port: number, deadlineMs: number): Promise<{ targets: ElectronCdpTarget[]; version: ElectronCdpVersion } | undefined> {
|
|
306
252
|
while (Date.now() <= deadlineMs) {
|
|
307
|
-
const version = parseCdpVersion(await
|
|
253
|
+
const version = parseCdpVersion(await fetchCdpJson(`http://127.0.0.1:${port}/json/version`));
|
|
308
254
|
if (version) {
|
|
309
|
-
const targets = parseCdpTargets(await
|
|
255
|
+
const targets = parseCdpTargets(await fetchCdpJson(`http://127.0.0.1:${port}/json/list`));
|
|
310
256
|
return { targets, version };
|
|
311
257
|
}
|
|
312
258
|
await sleep(ELECTRON_DEVTOOLS_POLL_INTERVAL_MS);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Keep small Electron presentation string helpers in one place.
|
|
3
|
+
* Responsibilities: Trim optional probe strings and bound them to model-visible lengths.
|
|
4
|
+
* Scope: Electron diagnostics/probe text helpers only; redaction and result formatting stay with their owning modules.
|
|
5
|
+
* Usage: Imported by Electron host probes and browser-run Electron diagnostics.
|
|
6
|
+
* Invariants/Assumptions: Empty or whitespace-only strings stay omitted, and truncation keeps the prior ellipsis behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function boundElectronProbeString(value: string | undefined, maxLength = 240): string | undefined {
|
|
10
|
+
const trimmed = value?.trim();
|
|
11
|
+
if (!trimmed) return undefined;
|
|
12
|
+
return trimmed.length > maxLength ? `${trimmed.slice(0, Math.max(0, maxLength - 3))}...` : trimmed;
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { access, stat } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
export async function pathExists(path: string): Promise<boolean> {
|
|
4
|
+
try {
|
|
5
|
+
await access(path);
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function directoryExists(path: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
return (await stat(path)).isDirectory();
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|