abelworkflow 0.2.0 → 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/bin/abelworkflow.mjs +6 -1
- package/lib/cli.mjs +543 -72
- package/lib/templates/codex/agents/default.toml +56 -0
- package/lib/templates/codex/agents/explorer.toml +63 -0
- package/lib/templates/codex/agents/planner.toml +89 -0
- package/lib/templates/codex/agents/reviewer.toml +71 -0
- package/lib/templates/codex/agents/worker.toml +67 -0
- package/lib/templates/codex/config-base.toml +95 -0
- package/package.json +2 -2
- package/skills/dev-browser/SKILL.md +61 -39
- package/skills/dev-browser/bun.lock +17 -0
- package/skills/dev-browser/package.json +2 -2
- package/skills/dev-browser/scripts/start.ts +279 -0
- package/skills/dev-browser/src/entrypoint.ts +157 -0
- package/skills/dev-browser/src/index.ts +4 -2
- package/skills/dev-browser/src/runtime.ts +147 -0
- package/skills/dev-browser/src/snapshot/browser-script.ts +2 -1
- package/skills/dev-browser/src/startup.test.ts +95 -0
- package/skills/dev-browser/src/startup.ts +153 -0
- package/skills/dev-browser/src/types.ts +1 -0
- package/skills/dev-browser/scripts/start-relay.ts +0 -32
- package/skills/dev-browser/scripts/start-server.ts +0 -117
- package/skills/dev-browser/server.sh +0 -24
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"@/*": "./src/*"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start-server": "npx tsx scripts/start
|
|
10
|
-
"start-extension": "npx tsx scripts/start
|
|
9
|
+
"start-server": "npx tsx scripts/start.ts standalone",
|
|
10
|
+
"start-extension": "npx tsx scripts/start.ts extension",
|
|
11
11
|
"dev": "npx tsx --watch src/index.ts",
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"test:watch": "vitest"
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { formatHttpUrl, parseEntrypointArgs, resolveHostForProbe } from "@/entrypoint.js";
|
|
6
|
+
import {
|
|
7
|
+
commandExists,
|
|
8
|
+
findAvailablePackageManager,
|
|
9
|
+
getMissingPackageDependencies,
|
|
10
|
+
isPlaywrightChromiumInstalled,
|
|
11
|
+
resolveSkillDirFromEntrypoint,
|
|
12
|
+
resolveRuntimePaths,
|
|
13
|
+
shouldUseShellForPackageCommands,
|
|
14
|
+
} from "@/runtime.js";
|
|
15
|
+
import {
|
|
16
|
+
ensurePlaywrightChromium,
|
|
17
|
+
preflightStandaloneStartup,
|
|
18
|
+
runEntrypoint,
|
|
19
|
+
} from "@/startup.js";
|
|
20
|
+
|
|
21
|
+
const runtimePaths = resolveRuntimePaths(resolveSkillDirFromEntrypoint(import.meta.url));
|
|
22
|
+
const useShell = shouldUseShellForPackageCommands(process.platform);
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const args = parseEntrypointArgs(process.argv.slice(2));
|
|
26
|
+
await ensureSkillDependencies();
|
|
27
|
+
|
|
28
|
+
await runEntrypoint(args, {
|
|
29
|
+
runtimePaths,
|
|
30
|
+
mkdir: mkdirSync,
|
|
31
|
+
serveStandalone: async (options) => {
|
|
32
|
+
const { serve } = await import("@/index.js");
|
|
33
|
+
return serve(options);
|
|
34
|
+
},
|
|
35
|
+
serveExtension: async (options) => {
|
|
36
|
+
const { serveRelay } = await import("@/relay.js");
|
|
37
|
+
return serveRelay(options);
|
|
38
|
+
},
|
|
39
|
+
registerShutdown,
|
|
40
|
+
keepAlive: () => new Promise(() => {}),
|
|
41
|
+
log: (line) => console.log(line),
|
|
42
|
+
ensureBrowser: () =>
|
|
43
|
+
ensurePlaywrightChromium({
|
|
44
|
+
isInstalled: () =>
|
|
45
|
+
isPlaywrightChromiumInstalled({
|
|
46
|
+
platform: process.platform,
|
|
47
|
+
env: process.env,
|
|
48
|
+
exists: existsSync,
|
|
49
|
+
readDir: readdirSync,
|
|
50
|
+
}),
|
|
51
|
+
findPackageManager: () =>
|
|
52
|
+
findAvailablePackageManager((command) =>
|
|
53
|
+
commandExists(command, (candidate) =>
|
|
54
|
+
spawnSync(candidate, ["--version"], {
|
|
55
|
+
stdio: "ignore",
|
|
56
|
+
shell: useShell,
|
|
57
|
+
windowsHide: useShell,
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
runCommand,
|
|
62
|
+
log: (line) => console.log(line),
|
|
63
|
+
}),
|
|
64
|
+
preflightStandalone: () =>
|
|
65
|
+
preflightStandaloneStartup(args, {
|
|
66
|
+
checkServer: async (host, port) => {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(formatHttpUrl(resolveHostForProbe(host), port), {
|
|
69
|
+
signal: AbortSignal.timeout(1000),
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return { ok: false };
|
|
73
|
+
}
|
|
74
|
+
const info = (await response.json()) as { wsEndpoint?: string };
|
|
75
|
+
return { ok: true, info };
|
|
76
|
+
} catch {
|
|
77
|
+
return { ok: false };
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
isPortInUse,
|
|
81
|
+
browserDataDir: runtimePaths.browserDataDir,
|
|
82
|
+
recoverStaleBrowser: ({ cdpPort, browserDataDir }) =>
|
|
83
|
+
recoverStaleDevBrowserChromium({
|
|
84
|
+
cdpPort,
|
|
85
|
+
browserDataDir,
|
|
86
|
+
isPortInUse,
|
|
87
|
+
log: (line) => console.log(line),
|
|
88
|
+
}),
|
|
89
|
+
log: (line) => console.log(line),
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ensureSkillDependencies() {
|
|
95
|
+
const packageJson = JSON.parse(readFileSync(join(runtimePaths.skillDir, "package.json"), "utf8")) as {
|
|
96
|
+
dependencies?: Record<string, string>;
|
|
97
|
+
};
|
|
98
|
+
const missingDependencies = getMissingPackageDependencies({
|
|
99
|
+
skillDir: runtimePaths.skillDir,
|
|
100
|
+
dependencies: Object.keys(packageJson.dependencies ?? {}),
|
|
101
|
+
exists: existsSync,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (missingDependencies.length === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("dev-browser dependencies not found. Installing local packages...");
|
|
109
|
+
await runCommand("npm", ["install"]);
|
|
110
|
+
console.log("dev-browser dependencies installed.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runCommand(command: string, args: string[]) {
|
|
114
|
+
await new Promise<void>((resolve, reject) => {
|
|
115
|
+
const child = spawn(command, args, {
|
|
116
|
+
cwd: runtimePaths.skillDir,
|
|
117
|
+
stdio: "inherit",
|
|
118
|
+
shell: useShell,
|
|
119
|
+
windowsHide: useShell,
|
|
120
|
+
});
|
|
121
|
+
child.on("error", reject);
|
|
122
|
+
child.on("close", (code) => {
|
|
123
|
+
if (code === 0) {
|
|
124
|
+
resolve();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function isPortInUse(port: number) {
|
|
133
|
+
const socket = await import("node:net");
|
|
134
|
+
return await new Promise<boolean>((resolve) => {
|
|
135
|
+
const server = socket.createServer();
|
|
136
|
+
server.once("error", () => resolve(true));
|
|
137
|
+
server.once("listening", () => {
|
|
138
|
+
server.close(() => resolve(false));
|
|
139
|
+
});
|
|
140
|
+
server.listen(port, "127.0.0.1");
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function recoverStaleDevBrowserChromium({
|
|
145
|
+
cdpPort,
|
|
146
|
+
browserDataDir,
|
|
147
|
+
isPortInUse,
|
|
148
|
+
log,
|
|
149
|
+
}: {
|
|
150
|
+
cdpPort: number;
|
|
151
|
+
browserDataDir: string;
|
|
152
|
+
isPortInUse: (port: number) => Promise<boolean>;
|
|
153
|
+
log: (line: string) => void;
|
|
154
|
+
}) {
|
|
155
|
+
const pid = findListeningPid(cdpPort);
|
|
156
|
+
if (!pid) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const commandLine = readProcessCommandLine(pid);
|
|
161
|
+
if (!commandLine || !isOwnedDevBrowserProcess(commandLine, browserDataDir, cdpPort)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
log(`Cleaning up stale dev-browser Chromium on CDP port ${cdpPort} (PID: ${pid})`);
|
|
166
|
+
if (!terminateProcess(pid)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const deadline = Date.now() + 3000;
|
|
171
|
+
while (Date.now() < deadline) {
|
|
172
|
+
if (!(await isPortInUse(cdpPort))) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return !(await isPortInUse(cdpPort));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findListeningPid(port: number): number | null {
|
|
182
|
+
if (process.platform === "win32") {
|
|
183
|
+
const result = runCapture("powershell.exe", [
|
|
184
|
+
"-NoProfile",
|
|
185
|
+
"-Command",
|
|
186
|
+
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -First 1`,
|
|
187
|
+
]);
|
|
188
|
+
return parsePid(result.stdout);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = runCapture("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
|
|
192
|
+
return parsePid(result.stdout);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readProcessCommandLine(pid: number): string | null {
|
|
196
|
+
if (process.platform === "win32") {
|
|
197
|
+
const result = runCapture("powershell.exe", [
|
|
198
|
+
"-NoProfile",
|
|
199
|
+
"-Command",
|
|
200
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
201
|
+
]);
|
|
202
|
+
return normalizeOutput(result.stdout);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = runCapture("ps", ["-p", String(pid), "-o", "args="]);
|
|
206
|
+
return normalizeOutput(result.stdout);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isOwnedDevBrowserProcess(commandLine: string, browserDataDir: string, cdpPort: number): boolean {
|
|
210
|
+
const normalizedCommandLine = normalizePathLike(commandLine);
|
|
211
|
+
const normalizedBrowserDataDir = normalizePathLike(browserDataDir);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
normalizedCommandLine.includes(`--remote-debugging-port=${cdpPort}`) &&
|
|
215
|
+
normalizedCommandLine.includes("--user-data-dir=") &&
|
|
216
|
+
normalizedCommandLine.includes(normalizedBrowserDataDir)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function terminateProcess(pid: number): boolean {
|
|
221
|
+
if (process.platform === "win32") {
|
|
222
|
+
return runCapture("taskkill", ["/PID", String(pid), "/T", "/F"]).status === 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
process.kill(pid, "SIGKILL");
|
|
227
|
+
return true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function runCapture(command: string, args: string[]) {
|
|
234
|
+
const result = spawnSync(command, args, {
|
|
235
|
+
cwd: runtimePaths.skillDir,
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
shell: false,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
status: result.status,
|
|
243
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function parsePid(stdout: string): number | null {
|
|
248
|
+
const value = normalizeOutput(stdout);
|
|
249
|
+
if (!value) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const pid = Number.parseInt(value.split(/\s+/)[0] ?? "", 10);
|
|
254
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeOutput(stdout: string): string | null {
|
|
258
|
+
const value = stdout.trim();
|
|
259
|
+
return value.length > 0 ? value : null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizePathLike(value: string): string {
|
|
263
|
+
return value.replaceAll("\\", "/").toLowerCase();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function registerShutdown(stop: () => Promise<void>) {
|
|
267
|
+
const shutdown = async () => {
|
|
268
|
+
await stop();
|
|
269
|
+
process.exit(0);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
process.on("SIGINT", shutdown);
|
|
273
|
+
process.on("SIGTERM", shutdown);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
main().catch((error) => {
|
|
277
|
+
console.error("Failed to start dev-browser:", error);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export type EntrypointMode = "standalone" | "extension";
|
|
2
|
+
|
|
3
|
+
export interface EntrypointArgs {
|
|
4
|
+
mode: EntrypointMode;
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
cdpPort: number;
|
|
8
|
+
headless: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StandaloneReadinessInfo {
|
|
12
|
+
mode: "standalone";
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
wsEndpoint: string;
|
|
16
|
+
tmpDir: string;
|
|
17
|
+
profileDir: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ExtensionReadinessInfo {
|
|
21
|
+
mode: "extension";
|
|
22
|
+
host: string;
|
|
23
|
+
port: number;
|
|
24
|
+
wsEndpoint: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ReadinessInfo = StandaloneReadinessInfo | ExtensionReadinessInfo;
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_HOST = "localhost";
|
|
30
|
+
const DEFAULT_PORT = 9222;
|
|
31
|
+
const DEFAULT_CDP_PORT = 9223;
|
|
32
|
+
|
|
33
|
+
export function formatHostForUrl(host: string): string {
|
|
34
|
+
const normalizedHost =
|
|
35
|
+
host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
36
|
+
return normalizedHost.includes(":") ? `[${normalizedHost}]` : normalizedHost;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatHttpUrl(host: string, port: number): string {
|
|
40
|
+
return `http://${formatHostForUrl(host)}:${port}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatWsUrl(host: string, port: number, path = ""): string {
|
|
44
|
+
return `ws://${formatHostForUrl(host)}:${port}${path}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveHostForProbe(host: string): string {
|
|
48
|
+
const normalizedHost =
|
|
49
|
+
host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
50
|
+
|
|
51
|
+
if (normalizedHost === "0.0.0.0") {
|
|
52
|
+
return "127.0.0.1";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (normalizedHost === "::") {
|
|
56
|
+
return "::1";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return normalizedHost;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseEntrypointArgs(
|
|
63
|
+
argv: string[],
|
|
64
|
+
env: Record<string, string | undefined> = process.env
|
|
65
|
+
): EntrypointArgs {
|
|
66
|
+
let index = 0;
|
|
67
|
+
let mode: EntrypointMode = "standalone";
|
|
68
|
+
|
|
69
|
+
const first = argv[0];
|
|
70
|
+
if (first && !first.startsWith("--")) {
|
|
71
|
+
if (first !== "standalone" && first !== "extension") {
|
|
72
|
+
throw new Error(`Unknown mode: ${first}`);
|
|
73
|
+
}
|
|
74
|
+
mode = first;
|
|
75
|
+
index = 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const args: EntrypointArgs = {
|
|
79
|
+
mode,
|
|
80
|
+
host: env.HOST?.trim() || DEFAULT_HOST,
|
|
81
|
+
port: readEnvPort(env.PORT, "PORT") ?? DEFAULT_PORT,
|
|
82
|
+
cdpPort: DEFAULT_CDP_PORT,
|
|
83
|
+
headless: env.HEADLESS?.trim().toLowerCase() === "true",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
while (index < argv.length) {
|
|
87
|
+
const token = argv[index++];
|
|
88
|
+
switch (token) {
|
|
89
|
+
case "--headless":
|
|
90
|
+
args.headless = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--host":
|
|
93
|
+
args.host = readValue(argv, index - 1);
|
|
94
|
+
index += 1;
|
|
95
|
+
break;
|
|
96
|
+
case "--port":
|
|
97
|
+
args.port = parsePort(readValue(argv, index - 1), "port");
|
|
98
|
+
index += 1;
|
|
99
|
+
break;
|
|
100
|
+
case "--cdp-port":
|
|
101
|
+
args.cdpPort = parsePort(readValue(argv, index - 1), "cdpPort");
|
|
102
|
+
index += 1;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unknown argument: ${token}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return args;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatReadinessLines(info: ReadinessInfo): string[] {
|
|
113
|
+
if (info.mode === "standalone") {
|
|
114
|
+
return [
|
|
115
|
+
"Dev browser server started",
|
|
116
|
+
` HTTP: ${formatHttpUrl(info.host, info.port)}`,
|
|
117
|
+
` WebSocket: ${info.wsEndpoint}`,
|
|
118
|
+
` Tmp directory: ${info.tmpDir}`,
|
|
119
|
+
` Profile directory: ${info.profileDir}`,
|
|
120
|
+
"",
|
|
121
|
+
"Ready",
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [
|
|
126
|
+
"CDP relay server started",
|
|
127
|
+
` HTTP: ${formatHttpUrl(info.host, info.port)}`,
|
|
128
|
+
` CDP endpoint: ${info.wsEndpoint}`,
|
|
129
|
+
` Extension endpoint: ${formatWsUrl(info.host, info.port, "/extension")}`,
|
|
130
|
+
"",
|
|
131
|
+
"Waiting for extension to connect...",
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readValue(argv: string[], optionIndex: number): string {
|
|
136
|
+
const value = argv[optionIndex + 1];
|
|
137
|
+
if (!value || value.startsWith("--")) {
|
|
138
|
+
throw new Error(`Missing value for ${argv[optionIndex]}`);
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parsePort(value: string, label: string): number {
|
|
144
|
+
const port = Number.parseInt(value, 10);
|
|
145
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
146
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
147
|
+
}
|
|
148
|
+
return port;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readEnvPort(value: string | undefined, label: string): number | undefined {
|
|
152
|
+
const normalized = value?.trim();
|
|
153
|
+
if (!normalized) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return parsePort(normalized, label);
|
|
157
|
+
}
|
|
@@ -3,6 +3,7 @@ import { chromium, type BrowserContext, type Page } from "playwright";
|
|
|
3
3
|
import { mkdirSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import type { Socket } from "net";
|
|
6
|
+
import { formatHttpUrl } from "./entrypoint";
|
|
6
7
|
import type {
|
|
7
8
|
ServeOptions,
|
|
8
9
|
GetPageRequest,
|
|
@@ -53,6 +54,7 @@ function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promi
|
|
|
53
54
|
|
|
54
55
|
export async function serve(options: ServeOptions = {}): Promise<DevBrowserServer> {
|
|
55
56
|
const port = options.port ?? 9222;
|
|
57
|
+
const host = options.host ?? "localhost";
|
|
56
58
|
const headless = options.headless ?? false;
|
|
57
59
|
const cdpPort = options.cdpPort ?? 9223;
|
|
58
60
|
const profileDir = options.profileDir;
|
|
@@ -191,8 +193,8 @@ export async function serve(options: ServeOptions = {}): Promise<DevBrowserServe
|
|
|
191
193
|
});
|
|
192
194
|
|
|
193
195
|
// Start the server
|
|
194
|
-
const server = app.listen(port, () => {
|
|
195
|
-
console.log(`HTTP API server running on
|
|
196
|
+
const server = app.listen(port, host, () => {
|
|
197
|
+
console.log(`HTTP API server running on ${formatHttpUrl(host, port)}`);
|
|
196
198
|
});
|
|
197
199
|
|
|
198
200
|
// Track active connections for clean shutdown
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
export interface CommandStatusResult {
|
|
5
|
+
status: number | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PackageManager {
|
|
9
|
+
name: "bun" | "pnpm" | "npm";
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChromiumInstallCheckOptions {
|
|
15
|
+
platform: NodeJS.Platform;
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
exists: (path: string) => boolean;
|
|
18
|
+
readDir: (path: string) => string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MissingPackageDependenciesOptions {
|
|
22
|
+
skillDir: string;
|
|
23
|
+
dependencies: string[];
|
|
24
|
+
exists: (path: string) => boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function commandExists(
|
|
28
|
+
command: string,
|
|
29
|
+
runCheck: (command: string) => CommandStatusResult
|
|
30
|
+
): boolean {
|
|
31
|
+
return runCheck(command).status === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findAvailablePackageManager(
|
|
35
|
+
hasCommand: (command: string) => boolean
|
|
36
|
+
): PackageManager | null {
|
|
37
|
+
const candidates: PackageManager[] = [
|
|
38
|
+
{
|
|
39
|
+
name: "bun",
|
|
40
|
+
command: "bunx",
|
|
41
|
+
args: ["playwright", "install", "chromium"],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "pnpm",
|
|
45
|
+
command: "pnpm",
|
|
46
|
+
args: ["exec", "playwright", "install", "chromium"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "npm",
|
|
50
|
+
command: "npx",
|
|
51
|
+
args: ["playwright", "install", "chromium"],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
const probe = candidate.name === "npm" ? "npm" : candidate.name;
|
|
57
|
+
if (hasCommand(probe)) {
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getPlaywrightInstallCommand(manager: PackageManager): string {
|
|
66
|
+
return [manager.command, ...manager.args].join(" ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getPlaywrightBrowserRoots({
|
|
70
|
+
platform,
|
|
71
|
+
env,
|
|
72
|
+
}: {
|
|
73
|
+
platform: NodeJS.Platform;
|
|
74
|
+
env: NodeJS.ProcessEnv;
|
|
75
|
+
}): string[] {
|
|
76
|
+
const explicitPath = env.PLAYWRIGHT_BROWSERS_PATH?.trim();
|
|
77
|
+
if (explicitPath) {
|
|
78
|
+
return [explicitPath];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (platform === "win32") {
|
|
82
|
+
const userProfile = env.USERPROFILE ?? env.HOME;
|
|
83
|
+
return userProfile ? [join(userProfile, "AppData", "Local", "ms-playwright")] : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const home = env.HOME ?? env.USERPROFILE;
|
|
87
|
+
return home ? [join(home, ".cache", "ms-playwright")] : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isPlaywrightChromiumInstalled({
|
|
91
|
+
platform,
|
|
92
|
+
env,
|
|
93
|
+
exists,
|
|
94
|
+
readDir,
|
|
95
|
+
}: ChromiumInstallCheckOptions): boolean {
|
|
96
|
+
for (const root of getPlaywrightBrowserRoots({ platform, env })) {
|
|
97
|
+
if (!exists(root)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const entries = readDir(root);
|
|
103
|
+
if (entries.some((entry) => entry.startsWith("chromium"))) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore unreadable directories and continue probing.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getMissingPackageDependencies({
|
|
115
|
+
skillDir,
|
|
116
|
+
dependencies,
|
|
117
|
+
exists,
|
|
118
|
+
}: MissingPackageDependenciesOptions): string[] {
|
|
119
|
+
return dependencies.filter(
|
|
120
|
+
(dependency) =>
|
|
121
|
+
!exists(join(skillDir, "node_modules", ...dependency.split("/"), "package.json"))
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveRuntimePaths(skillDir: string) {
|
|
126
|
+
const tmpDir = join(skillDir, "tmp");
|
|
127
|
+
const profileDir = join(skillDir, "profiles");
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
skillDir,
|
|
131
|
+
tmpDir,
|
|
132
|
+
profileDir,
|
|
133
|
+
browserDataDir: join(profileDir, "browser-data"),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resolveImportMetaDir(moduleUrl: string): string {
|
|
138
|
+
return dirname(fileURLToPath(moduleUrl));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolveSkillDirFromEntrypoint(moduleUrl: string): string {
|
|
142
|
+
return dirname(resolveImportMetaDir(moduleUrl));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function shouldUseShellForPackageCommands(platform: NodeJS.Platform): boolean {
|
|
146
|
+
return platform === "win32";
|
|
147
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from "fs";
|
|
13
13
|
import * as path from "path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
|
|
15
16
|
// Cache the bundled script
|
|
16
17
|
let cachedScript: string | null = null;
|
|
@@ -26,7 +27,7 @@ export function getSnapshotScript(): string {
|
|
|
26
27
|
if (cachedScript) return cachedScript;
|
|
27
28
|
|
|
28
29
|
// Read the compiled JavaScript files
|
|
29
|
-
const snapshotDir = path.dirname(
|
|
30
|
+
const snapshotDir = path.dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
|
|
31
32
|
// For now, we'll inline the functions directly
|
|
32
33
|
// In production, we could use a bundler like esbuild to create a single file
|