abelworkflow 0.6.0 → 0.6.2

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/lib/cli.mjs CHANGED
@@ -1086,9 +1086,16 @@ function commandExists(command) {
1086
1086
  return result.status === 0;
1087
1087
  }
1088
1088
 
1089
+ function getRunCommandSpawnOptions(platform = getPlatform()) {
1090
+ return {
1091
+ stdio: "inherit",
1092
+ shell: platform === "win32"
1093
+ };
1094
+ }
1095
+
1089
1096
  async function runCommand(command, args) {
1090
1097
  await new Promise((resolvePromise, rejectPromise) => {
1091
- const child = spawn(command, args, { stdio: "inherit" });
1098
+ const child = spawn(command, args, getRunCommandSpawnOptions());
1092
1099
  child.on("error", rejectPromise);
1093
1100
  child.on("close", (code) => {
1094
1101
  if (code === 0) {
@@ -2029,6 +2036,7 @@ async function main() {
2029
2036
 
2030
2037
  export {
2031
2038
  buildCodexConfigContent,
2039
+ getRunCommandSpawnOptions,
2032
2040
  main,
2033
2041
  mergeCodexAuthData,
2034
2042
  mergeClaudeSettingsWithDefaults,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "abelworkflow": "./bin/abelworkflow.mjs"
7
+ "abelworkflow": "bin/abelworkflow.mjs"
8
8
  },
9
9
  "files": [
10
10
  "bin",
@@ -15,64 +15,80 @@ Browser automation that maintains page state across script executions. Write sma
15
15
 
16
16
  ## Setup
17
17
 
18
- Two modes available. Ask the user if unclear which to use.
18
+ Two supported startup modes are available on **Linux** and **native Windows**. Run the following commands from the installed `dev-browser` skill directory. Bash, Git Bash, and WSL are not required.
19
19
 
20
- ### Resolving SKILL_DIR
20
+ ### Standalone Mode (Default)
21
21
 
22
- Prepend this to every Bash snippet (each starts a fresh shell). All examples below use `### SKILL_DIR ###` as a placeholder for this block:
22
+ Launches a new Chromium browser for fresh automation sessions.
23
23
 
24
- ```bash
25
- SKILL_DIR=$(bash skills/dev-browser/resolve-skill-dir.sh 2>/dev/null) || { echo "Skill not found: dev-browser" >&2; exit 1; }
24
+ ```text
25
+ npx tsx scripts/start.ts standalone
26
26
  ```
27
27
 
28
- ### Standalone Mode (Default)
29
-
30
- Launches a new Chromium browser for fresh automation sessions.
28
+ Add `--headless` if needed:
31
29
 
32
- ```bash
33
- ### SKILL_DIR ###
34
- bash "$SKILL_DIR/server.sh" &
30
+ ```text
31
+ npx tsx scripts/start.ts standalone --headless
35
32
  ```
36
33
 
37
- Add `--headless` flag if user requests it. **Wait for the `Ready` message before running scripts.**
34
+ Wait for the stable readiness line `Ready` before running scripts.
38
35
 
39
36
  ### Extension Mode
40
37
 
41
- Connects to user's existing Chrome browser. Use this when:
38
+ Connects to the user's existing Chrome browser. Use this when:
42
39
 
43
40
  - The user is already logged into sites and wants you to do things behind an authed experience that isn't local dev.
44
- - The user asks you to use the extension
45
-
46
- **Important**: The core flow is still the same. You create named pages inside of their browser.
41
+ - The user asks you to use the extension.
47
42
 
48
- **Start the relay server:**
43
+ Start the relay server with:
49
44
 
50
- ```bash
51
- ### SKILL_DIR ###
52
- (cd "$SKILL_DIR" && npm i && npm run start-extension) &
45
+ ```text
46
+ npx tsx scripts/start.ts extension
53
47
  ```
54
48
 
55
- Wait for `Waiting for extension to connect...` followed by `Extension connected` in the console. To know that a client has connected and the browser is ready to be controlled.
56
- **Workflow:**
57
-
58
- 1. Scripts call `client.page("name")` just like the normal mode to create new pages / connect to existing ones.
59
- 2. Automation runs on the user's actual browser session
49
+ Wait for `Waiting for extension to connect...`. Once the browser extension attaches, the relay logs `Extension connected`.
60
50
 
61
51
  If the extension hasn't connected yet, tell the user to launch and activate it. Download link: https://github.com/SawyerHood/dev-browser/releases
62
52
 
53
+ ## Support Matrix
54
+
55
+ | Mode | Linux | Native Windows | Readiness signal | Notes |
56
+ |------|-------|----------------|------------------|-------|
57
+ | standalone mode | Supported | Supported | `Ready` | Launches a managed Chromium profile under `profiles/` |
58
+ | extension mode | Supported | Supported | `Waiting for extension to connect...` then `Extension connected` | Requires the external browser extension to attach |
59
+
60
+ ## Verification Checklist
61
+
62
+ - Start `standalone mode` with `npx tsx scripts/start.ts standalone`
63
+ - Observe the readiness line `Ready`
64
+ - Connect with `connect()` and create a named page
65
+ - Start `extension mode` with `npx tsx scripts/start.ts extension`
66
+ - Observe `Waiting for extension to connect...`
67
+ - Attach the browser extension and confirm `Extension connected`
68
+
69
+ ## Known Differences
70
+
71
+ - `standalone mode` launches and owns its own Chromium profile under this skill directory.
72
+ - `extension mode` depends on the external browser extension and the user's existing Chrome session.
73
+ - In `extension mode`, relay readiness means the server is waiting for the extension; it does not imply browser control is available until `Extension connected` appears.
74
+ - `standalone mode` is the default path for deterministic local automation; `extension mode` is for working inside an already-authenticated browser.
75
+
76
+ ## Non-goal Environments
77
+
78
+ - `WSL` and `Git Bash` are not part of the supported Windows path for this skill.
79
+ - Native Windows support means PowerShell / Command Prompt can use the documented entrypoints directly.
80
+ - If a user runs inside WSL or Git Bash, treat that as a separate environment rather than the official Windows support contract.
81
+
63
82
  ## Writing Scripts
64
83
 
65
- > **Each standalone Bash snippet resolves `SKILL_DIR` inside the same snippet before changing directories.** The `@/` import alias requires the skill root's config.
84
+ Use small TypeScript files under `tmp/` instead of shell heredocs. Run them from the same `dev-browser` skill directory so the `@/` import alias resolves correctly.
66
85
 
67
- Execute scripts inline using heredocs:
86
+ Example script (`tmp/example.ts`):
68
87
 
69
- ```bash
70
- ### SKILL_DIR ###
71
- (cd "$SKILL_DIR" && npx tsx <<'EOF'
88
+ ```typescript
72
89
  import { connect, waitForPageLoad } from "@/client.js";
73
90
 
74
91
  const client = await connect();
75
- // Create page with custom viewport size (optional)
76
92
  const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
77
93
 
78
94
  await page.goto("https://example.com");
@@ -80,8 +96,12 @@ await waitForPageLoad(page);
80
96
 
81
97
  console.log({ title: await page.title(), url: page.url() });
82
98
  await client.disconnect();
83
- EOF
84
- )
99
+ ```
100
+
101
+ Run it with:
102
+
103
+ ```text
104
+ npx tsx tmp/example.ts
85
105
  ```
86
106
 
87
107
  **Write to `tmp/` files only when** the script needs reuse, is complex, or user explicitly requests it.
@@ -202,11 +222,9 @@ await element.click();
202
222
 
203
223
  ## Error Recovery
204
224
 
205
- Page state persists after failures. Re-resolve `SKILL_DIR` (see top of file) before debugging:
225
+ Page state persists after failures. To inspect the current state, save a short script such as `tmp/debug.ts` and run it from the `dev-browser` skill directory:
206
226
 
207
- ```bash
208
- ### SKILL_DIR ###
209
- (cd "$SKILL_DIR" && npx tsx <<'EOF'
227
+ ```typescript
210
228
  import { connect } from "@/client.js";
211
229
 
212
230
  const client = await connect();
@@ -220,6 +238,10 @@ console.log({
220
238
  });
221
239
 
222
240
  await client.disconnect();
223
- EOF
224
- )
241
+ ```
242
+
243
+ Run it with:
244
+
245
+ ```text
246
+ npx tsx tmp/debug.ts
225
247
  ```
@@ -5,14 +5,21 @@
5
5
  "": {
6
6
  "name": "dev-browser",
7
7
  "dependencies": {
8
+ "@hono/node-server": "^1.19.7",
9
+ "@hono/node-ws": "^1.2.0",
8
10
  "express": "^4.21.0",
11
+ "hono": "^4.11.1",
9
12
  "playwright": "^1.49.0",
10
13
  },
11
14
  "devDependencies": {
12
15
  "@types/express": "^5.0.0",
13
16
  "tsx": "^4.21.0",
17
+ "typescript": "^5.0.0",
14
18
  "vitest": "^2.1.0",
15
19
  },
20
+ "optionalDependencies": {
21
+ "@rollup/rollup-linux-x64-gnu": "^4.0.0",
22
+ },
16
23
  },
17
24
  },
18
25
  "packages": {
@@ -68,6 +75,10 @@
68
75
 
69
76
  "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="],
70
77
 
78
+ "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
79
+
80
+ "@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
81
+
71
82
  "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
72
83
 
73
84
  "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
@@ -234,6 +245,8 @@
234
245
 
235
246
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
236
247
 
248
+ "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="],
249
+
237
250
  "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
238
251
 
239
252
  "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -342,6 +355,8 @@
342
355
 
343
356
  "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
344
357
 
358
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
359
+
345
360
  "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
346
361
 
347
362
  "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -358,6 +373,8 @@
358
373
 
359
374
  "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
360
375
 
376
+ "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
377
+
361
378
  "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
362
379
 
363
380
  "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -6,8 +6,8 @@
6
6
  "@/*": "./src/*"
7
7
  },
8
8
  "scripts": {
9
- "start-server": "npx tsx scripts/start-server.ts",
10
- "start-extension": "npx tsx scripts/start-relay.ts",
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 port ${port}`);
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(new URL(import.meta.url).pathname);
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
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ DEFAULT_HOST,
5
+ formatHttpUrl,
6
+ formatReadinessLines,
7
+ parseEntrypointArgs,
8
+ resolveHostForProbe,
9
+ } from "./entrypoint.js";
10
+ import { preflightStandaloneStartup } from "./startup.js";
11
+
12
+ describe("entrypoint host handling", () => {
13
+ it("defaults standalone host to localhost", () => {
14
+ const args = parseEntrypointArgs([], {});
15
+
16
+ expect(args.host).toBe(DEFAULT_HOST);
17
+ expect(args.host).toBe("localhost");
18
+ });
19
+
20
+ it("formats IPv6 hosts in readiness output", () => {
21
+ expect(formatHttpUrl("::1", 9222)).toBe("http://[::1]:9222");
22
+
23
+ expect(
24
+ formatReadinessLines({
25
+ mode: "standalone",
26
+ host: "::1",
27
+ port: 9222,
28
+ wsEndpoint: "ws://127.0.0.1:9223/devtools/browser/test",
29
+ tmpDir: "/tmp/dev-browser",
30
+ profileDir: "/tmp/dev-browser/profile",
31
+ })
32
+ ).toContain(" HTTP: http://[::1]:9222");
33
+ });
34
+
35
+ it("normalizes wildcard hosts to a reachable probe target", () => {
36
+ expect(resolveHostForProbe("0.0.0.0")).toBe("127.0.0.1");
37
+ expect(resolveHostForProbe("::")).toBe("::1");
38
+ expect(resolveHostForProbe("[::1]")).toBe("::1");
39
+ expect(resolveHostForProbe("localhost")).toBe("localhost");
40
+ });
41
+ });
42
+
43
+ describe("preflightStandaloneStartup", () => {
44
+ it("checks the configured host before starting", async () => {
45
+ const checkServer = vi.fn().mockResolvedValue({ ok: false });
46
+ const isPortInUse = vi.fn().mockResolvedValue(false);
47
+ const log = vi.fn();
48
+
49
+ await expect(
50
+ preflightStandaloneStartup(
51
+ {
52
+ mode: "standalone",
53
+ host: "::1",
54
+ port: 9222,
55
+ cdpPort: 9223,
56
+ headless: false,
57
+ },
58
+ {
59
+ checkServer,
60
+ isPortInUse,
61
+ log,
62
+ }
63
+ )
64
+ ).resolves.toBe(true);
65
+
66
+ expect(checkServer).toHaveBeenCalledWith("::1", 9222);
67
+ });
68
+
69
+ it("short-circuits when the configured host already has a server", async () => {
70
+ const checkServer = vi.fn().mockResolvedValue({ ok: true, info: { wsEndpoint: "ws://test" } });
71
+ const isPortInUse = vi.fn();
72
+ const log = vi.fn();
73
+
74
+ await expect(
75
+ preflightStandaloneStartup(
76
+ {
77
+ mode: "standalone",
78
+ host: "localhost",
79
+ port: 9222,
80
+ cdpPort: 9223,
81
+ headless: false,
82
+ },
83
+ {
84
+ checkServer,
85
+ isPortInUse,
86
+ log,
87
+ }
88
+ )
89
+ ).resolves.toBe(false);
90
+
91
+ expect(checkServer).toHaveBeenCalledWith("localhost", 9222);
92
+ expect(isPortInUse).not.toHaveBeenCalled();
93
+ expect(log).toHaveBeenCalledWith("Server already running on port 9222");
94
+ });
95
+ });
@@ -0,0 +1,153 @@
1
+ import { formatReadinessLines, type EntrypointArgs } from "./entrypoint.js";
2
+ import type { PackageManager } from "./runtime.js";
3
+
4
+ export interface RuntimePaths {
5
+ skillDir: string;
6
+ tmpDir: string;
7
+ profileDir: string;
8
+ browserDataDir: string;
9
+ }
10
+
11
+ interface StandaloneServer {
12
+ port: number;
13
+ wsEndpoint: string;
14
+ stop: () => Promise<void>;
15
+ }
16
+
17
+ interface ExtensionServer {
18
+ port: number;
19
+ wsEndpoint: string;
20
+ stop: () => Promise<void>;
21
+ }
22
+
23
+ export interface RunEntrypointDeps {
24
+ runtimePaths: RuntimePaths;
25
+ mkdir: (path: string, options: { recursive: true }) => void;
26
+ serveStandalone: (options: {
27
+ port: number;
28
+ host: string;
29
+ headless: boolean;
30
+ cdpPort: number;
31
+ profileDir: string;
32
+ }) => Promise<StandaloneServer>;
33
+ serveExtension: (options: { port: number; host: string }) => Promise<ExtensionServer>;
34
+ registerShutdown: (stop: () => Promise<void>) => void;
35
+ keepAlive: () => Promise<void>;
36
+ log: (line: string) => void;
37
+ ensureBrowser?: () => Promise<void>;
38
+ preflightStandalone?: () => Promise<boolean>;
39
+ }
40
+
41
+ export interface EnsurePlaywrightChromiumDeps {
42
+ isInstalled: () => boolean;
43
+ findPackageManager: () => PackageManager | null;
44
+ runCommand: (command: string, args: string[]) => Promise<void>;
45
+ log: (line: string) => void;
46
+ }
47
+
48
+ export interface PreflightStandaloneStartupDeps {
49
+ checkServer: (host: string, port: number) => Promise<{ ok: boolean; info?: { wsEndpoint?: string } }>;
50
+ isPortInUse: (port: number) => Promise<boolean>;
51
+ browserDataDir?: string;
52
+ recoverStaleBrowser?: (options: { cdpPort: number; browserDataDir: string }) => Promise<boolean>;
53
+ log: (line: string) => void;
54
+ }
55
+
56
+ export async function preflightStandaloneStartup(
57
+ args: EntrypointArgs,
58
+ deps: PreflightStandaloneStartupDeps
59
+ ): Promise<boolean> {
60
+ const serverCheck = await deps.checkServer(args.host, args.port);
61
+ if (serverCheck.ok) {
62
+ deps.log(`Server already running on port ${args.port}`);
63
+ return false;
64
+ }
65
+
66
+ if (await deps.isPortInUse(args.cdpPort)) {
67
+ const recovered =
68
+ deps.browserDataDir && deps.recoverStaleBrowser
69
+ ? await deps.recoverStaleBrowser({
70
+ cdpPort: args.cdpPort,
71
+ browserDataDir: deps.browserDataDir,
72
+ })
73
+ : false;
74
+
75
+ if (!recovered && (await deps.isPortInUse(args.cdpPort))) {
76
+ throw new Error(`CDP port ${args.cdpPort} is already in use by another process`);
77
+ }
78
+ }
79
+
80
+ return true;
81
+ }
82
+
83
+ export async function ensurePlaywrightChromium(deps: EnsurePlaywrightChromiumDeps) {
84
+ if (deps.isInstalled()) {
85
+ deps.log("Playwright Chromium already installed.");
86
+ return;
87
+ }
88
+
89
+ deps.log("Playwright Chromium not found. Installing (this may take a minute)...");
90
+ const manager = deps.findPackageManager();
91
+ if (!manager) {
92
+ throw new Error("No package manager found (tried bun, pnpm, npm)");
93
+ }
94
+
95
+ deps.log(`Using ${manager.name} to install Playwright...`);
96
+ await deps.runCommand(manager.command, manager.args);
97
+ deps.log("Chromium installed successfully.");
98
+ }
99
+
100
+ export async function runEntrypoint(args: EntrypointArgs, deps: RunEntrypointDeps) {
101
+ deps.mkdir(deps.runtimePaths.tmpDir, { recursive: true });
102
+
103
+ if (args.mode === "extension") {
104
+ const server = await deps.serveExtension({
105
+ port: args.port,
106
+ host: args.host,
107
+ });
108
+
109
+ for (const line of formatReadinessLines({
110
+ mode: "extension",
111
+ host: args.host,
112
+ port: args.port,
113
+ wsEndpoint: server.wsEndpoint,
114
+ })) {
115
+ deps.log(line);
116
+ }
117
+
118
+ deps.registerShutdown(() => server.stop());
119
+ await deps.keepAlive();
120
+ return;
121
+ }
122
+
123
+ deps.mkdir(deps.runtimePaths.profileDir, { recursive: true });
124
+
125
+ const shouldStart = await deps.preflightStandalone?.();
126
+ if (shouldStart === false) {
127
+ return;
128
+ }
129
+
130
+ await deps.ensureBrowser?.();
131
+
132
+ const server = await deps.serveStandalone({
133
+ port: args.port,
134
+ host: args.host,
135
+ headless: args.headless,
136
+ cdpPort: args.cdpPort,
137
+ profileDir: deps.runtimePaths.profileDir,
138
+ });
139
+
140
+ for (const line of formatReadinessLines({
141
+ mode: "standalone",
142
+ host: args.host,
143
+ port: args.port,
144
+ wsEndpoint: server.wsEndpoint,
145
+ tmpDir: deps.runtimePaths.tmpDir,
146
+ profileDir: deps.runtimePaths.profileDir,
147
+ })) {
148
+ deps.log(line);
149
+ }
150
+
151
+ deps.registerShutdown(() => server.stop());
152
+ await deps.keepAlive();
153
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  export interface ServeOptions {
4
4
  port?: number;
5
+ host?: string;
5
6
  headless?: boolean;
6
7
  cdpPort?: number;
7
8
  /** Directory to store persistent browser profiles (cookies, localStorage, etc.) */
@@ -1,4 +0,0 @@
1
- # Context7 API Key Configuration
2
- # Get your API key from: https://context7.com/dashboard
3
-
4
- CONTEXT7_API_KEY=ctx7sk-d4d4d513-e3ae-44ae-b67d-30c046898ecf
@@ -1,32 +0,0 @@
1
- /**
2
- * Start the CDP relay server for Chrome extension mode
3
- *
4
- * Usage: npm run start-extension
5
- */
6
-
7
- import { serveRelay } from "@/relay.js";
8
-
9
- const PORT = parseInt(process.env.PORT || "9222", 10);
10
- const HOST = process.env.HOST || "127.0.0.1";
11
-
12
- async function main() {
13
- const server = await serveRelay({
14
- port: PORT,
15
- host: HOST,
16
- });
17
-
18
- // Handle shutdown
19
- const shutdown = async () => {
20
- console.log("\nShutting down relay server...");
21
- await server.stop();
22
- process.exit(0);
23
- };
24
-
25
- process.on("SIGINT", shutdown);
26
- process.on("SIGTERM", shutdown);
27
- }
28
-
29
- main().catch((err) => {
30
- console.error("Failed to start relay server:", err);
31
- process.exit(1);
32
- });
@@ -1,117 +0,0 @@
1
- import { serve } from "@/index.js";
2
- import { execSync } from "child_process";
3
- import { mkdirSync, existsSync, readdirSync } from "fs";
4
- import { join, dirname } from "path";
5
- import { fileURLToPath } from "url";
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const tmpDir = join(__dirname, "..", "tmp");
9
- const profileDir = join(__dirname, "..", "profiles");
10
-
11
- // Create tmp and profile directories if they don't exist
12
- console.log("Creating tmp directory...");
13
- mkdirSync(tmpDir, { recursive: true });
14
- console.log("Creating profiles directory...");
15
- mkdirSync(profileDir, { recursive: true });
16
-
17
- // Install Playwright browsers if not already installed
18
- console.log("Checking Playwright browser installation...");
19
-
20
- function findPackageManager(): { name: string; command: string } | null {
21
- const managers = [
22
- { name: "bun", command: "bunx playwright install chromium" },
23
- { name: "pnpm", command: "pnpm exec playwright install chromium" },
24
- { name: "npm", command: "npx playwright install chromium" },
25
- ];
26
-
27
- for (const manager of managers) {
28
- try {
29
- execSync(`which ${manager.name}`, { stdio: "ignore" });
30
- return manager;
31
- } catch {
32
- // Package manager not found, try next
33
- }
34
- }
35
- return null;
36
- }
37
-
38
- function isChromiumInstalled(): boolean {
39
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
40
- const playwrightCacheDir = join(homeDir, ".cache", "ms-playwright");
41
-
42
- if (!existsSync(playwrightCacheDir)) {
43
- return false;
44
- }
45
-
46
- // Check for chromium directories (e.g., chromium-1148, chromium_headless_shell-1148)
47
- try {
48
- const entries = readdirSync(playwrightCacheDir);
49
- return entries.some((entry) => entry.startsWith("chromium"));
50
- } catch {
51
- return false;
52
- }
53
- }
54
-
55
- try {
56
- if (!isChromiumInstalled()) {
57
- console.log("Playwright Chromium not found. Installing (this may take a minute)...");
58
-
59
- const pm = findPackageManager();
60
- if (!pm) {
61
- throw new Error("No package manager found (tried bun, pnpm, npm)");
62
- }
63
-
64
- console.log(`Using ${pm.name} to install Playwright...`);
65
- execSync(pm.command, { stdio: "inherit" });
66
- console.log("Chromium installed successfully.");
67
- } else {
68
- console.log("Playwright Chromium already installed.");
69
- }
70
- } catch (error) {
71
- console.error("Failed to install Playwright browsers:", error);
72
- console.log("You may need to run: npx playwright install chromium");
73
- }
74
-
75
- // Check if server is already running
76
- console.log("Checking for existing servers...");
77
- try {
78
- const res = await fetch("http://localhost:9222", {
79
- signal: AbortSignal.timeout(1000),
80
- });
81
- if (res.ok) {
82
- console.log("Server already running on port 9222");
83
- process.exit(0);
84
- }
85
- } catch {
86
- // Server not running, continue to start
87
- }
88
-
89
- // Clean up stale CDP port if HTTP server isn't running (crash recovery)
90
- // This handles the case where Node crashed but Chrome is still running on 9223
91
- try {
92
- const pid = execSync("lsof -ti:9223", { encoding: "utf-8" }).trim();
93
- if (pid) {
94
- console.log(`Cleaning up stale Chrome process on CDP port 9223 (PID: ${pid})`);
95
- execSync(`kill -9 ${pid}`);
96
- }
97
- } catch {
98
- // No process on CDP port, which is expected
99
- }
100
-
101
- console.log("Starting dev browser server...");
102
- const headless = process.env.HEADLESS === "true";
103
- const server = await serve({
104
- port: 9222,
105
- headless,
106
- profileDir,
107
- });
108
-
109
- console.log(`Dev browser server started`);
110
- console.log(` WebSocket: ${server.wsEndpoint}`);
111
- console.log(` Tmp directory: ${tmpDir}`);
112
- console.log(` Profile directory: ${profileDir}`);
113
- console.log(`\nReady`);
114
- console.log(`\nPress Ctrl+C to stop`);
115
-
116
- // Keep the process running
117
- await new Promise(() => {});
@@ -1,24 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Get the directory where this script is located
4
- SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
5
-
6
- # Change to the script directory
7
- cd "$SCRIPT_DIR"
8
-
9
- # Parse command line arguments
10
- HEADLESS=false
11
- while [[ "$#" -gt 0 ]]; do
12
- case $1 in
13
- --headless) HEADLESS=true ;;
14
- *) echo "Unknown parameter: $1"; exit 1 ;;
15
- esac
16
- shift
17
- done
18
-
19
- echo "Installing dependencies..."
20
- npm install
21
-
22
- echo "Starting dev-browser server..."
23
- export HEADLESS=$HEADLESS
24
- npx tsx scripts/start-server.ts
@@ -1,2 +0,0 @@
1
- OPENAI_API_KEY=dummy
2
- PE_MODEL=