Skip to content

Commit e5e0772

Browse files
committed
feat(cli): add remote debugging port option
Adds a Chromium remote debugging port flag for preview and play. The flag is only passed when launching an explicit browser/profile. HyperFrames still does not own CDP automation.
1 parent 07de7e6 commit e5e0772

4 files changed

Lines changed: 197 additions & 7 deletions

File tree

packages/cli/src/commands/play.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { resolve, dirname } from "node:path";
1313
import * as clack from "@clack/prompts";
1414
import { c } from "../ui/colors.js";
1515
import { resolveProject } from "../utils/project.js";
16-
import { openBrowser } from "../utils/openBrowser.js";
16+
import { openBrowser, parseRemoteDebuggingPort } from "../utils/openBrowser.js";
1717

1818
export default defineCommand({
1919
meta: { name: "play", description: "Play a composition in a lightweight browser player" },
@@ -33,6 +33,10 @@ export default defineCommand({
3333
type: "string",
3434
description: "Chromium-compatible user data directory (requires --browser-path)",
3535
},
36+
"remote-debugging-port": {
37+
type: "string",
38+
description: "Chromium remote debugging port (requires --browser-path and --user-data-dir)",
39+
},
3640
},
3741
async run({ args }) {
3842
const project = resolveProject(args.dir);
@@ -44,6 +48,19 @@ export default defineCommand({
4448
process.exitCode = 1;
4549
return;
4650
}
51+
// Validation: --remote-debugging-port requires --browser-path and --user-data-dir
52+
if (args["remote-debugging-port"]) {
53+
if (!args["browser-path"]) {
54+
clack.log.error("--remote-debugging-port requires --browser-path");
55+
process.exitCode = 1;
56+
return;
57+
}
58+
if (!args["user-data-dir"]) {
59+
clack.log.error("--remote-debugging-port requires --user-data-dir");
60+
process.exitCode = 1;
61+
return;
62+
}
63+
}
4764

4865
// Resolve runtime path — same logic as studioServer.ts
4966
const runtimePath = resolveRuntimePath();
@@ -168,10 +185,22 @@ export default defineCommand({
168185
console.log();
169186
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
170187
console.log();
188+
let remoteDebuggingPort: number | undefined;
189+
if (args["remote-debugging-port"]) {
190+
try {
191+
remoteDebuggingPort = parseRemoteDebuggingPort(args["remote-debugging-port"]);
192+
} catch (err) {
193+
clack.log.error((err as Error).message);
194+
process.exitCode = 1;
195+
return;
196+
}
197+
}
198+
171199
if (args.open) {
172200
void openBrowser(url, {
173201
browserPath: args["browser-path"] as string | undefined,
174202
userDataDir: args["user-data-dir"] as string | undefined,
203+
remoteDebuggingPort,
175204
});
176205
}
177206

packages/cli/src/commands/preview.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { createRequire } from "node:module";
1919
import * as clack from "@clack/prompts";
2020
import { c } from "../ui/colors.js";
2121
import { isDevMode } from "../utils/env.js";
22-
import { openBrowser } from "../utils/openBrowser.js";
22+
import { openBrowser, parseRemoteDebuggingPort } from "../utils/openBrowser.js";
2323
import { lintProject } from "../utils/lintProject.js";
2424
import { formatLintFindings } from "../utils/lintFormat.js";
2525
import {
@@ -62,6 +62,10 @@ export default defineCommand({
6262
type: "string",
6363
description: "Chromium-compatible user data directory (requires --browser-path)",
6464
},
65+
"remote-debugging-port": {
66+
type: "string",
67+
description: "Chromium remote debugging port (requires --browser-path and --user-data-dir)",
68+
},
6569
},
6670
async run({ args }) {
6771
const startPort = parseInt(args.port ?? "3002", 10);
@@ -122,18 +126,53 @@ export default defineCommand({
122126
process.exitCode = 1;
123127
return;
124128
}
129+
// Validation: --remote-debugging-port requires --browser-path and --user-data-dir
130+
if (args["remote-debugging-port"]) {
131+
if (!args["browser-path"]) {
132+
clack.log.error("--remote-debugging-port requires --browser-path");
133+
process.exitCode = 1;
134+
return;
135+
}
136+
if (!args["user-data-dir"]) {
137+
clack.log.error("--remote-debugging-port requires --user-data-dir");
138+
process.exitCode = 1;
139+
return;
140+
}
141+
}
125142

126143
const noOpen = !args.open;
127144
const browserPath = args["browser-path"] as string | undefined;
128145
const userDataDir = args["user-data-dir"] as string | undefined;
146+
let remoteDebuggingPort: number | undefined;
147+
if (args["remote-debugging-port"]) {
148+
try {
149+
remoteDebuggingPort = parseRemoteDebuggingPort(args["remote-debugging-port"]);
150+
} catch (err) {
151+
clack.log.error((err as Error).message);
152+
process.exitCode = 1;
153+
return;
154+
}
155+
}
129156

130157
if (isDevMode()) {
131-
return runDevMode(dir, { projectName, noOpen, browserPath, userDataDir });
158+
return runDevMode(dir, {
159+
projectName,
160+
noOpen,
161+
browserPath,
162+
userDataDir,
163+
remoteDebuggingPort,
164+
});
132165
}
133166

134167
// If @hyperframes/studio is installed locally, use Vite for full HMR
135168
if (hasLocalStudio(dir)) {
136-
return runLocalStudioMode(dir, { projectName, noOpen, browserPath, userDataDir });
169+
return runLocalStudioMode(dir, {
170+
projectName,
171+
noOpen,
172+
browserPath,
173+
userDataDir,
174+
remoteDebuggingPort,
175+
});
137176
}
138177

139178
const forceNew = !!args["force-new"];
@@ -143,6 +182,7 @@ export default defineCommand({
143182
noOpen,
144183
browserPath,
145184
userDataDir,
185+
remoteDebuggingPort,
146186
});
147187
},
148188
});
@@ -152,7 +192,13 @@ export default defineCommand({
152192
*/
153193
async function runDevMode(
154194
dir: string,
155-
options?: { projectName?: string; noOpen?: boolean; browserPath?: string; userDataDir?: string },
195+
options?: {
196+
projectName?: string;
197+
noOpen?: boolean;
198+
browserPath?: string;
199+
userDataDir?: string;
200+
remoteDebuggingPort?: number;
201+
},
156202
): Promise<void> {
157203
// Find monorepo root by navigating from packages/cli/src/commands/
158204
const thisFile = fileURLToPath(import.meta.url);
@@ -222,6 +268,7 @@ async function runDevMode(
222268
openBrowser(urlToOpen, {
223269
browserPath: options?.browserPath,
224270
userDataDir: options?.userDataDir,
271+
remoteDebuggingPort: options?.remoteDebuggingPort,
225272
});
226273
}
227274

@@ -275,7 +322,13 @@ function hasLocalStudio(dir: string): boolean {
275322
*/
276323
async function runLocalStudioMode(
277324
dir: string,
278-
options?: { projectName?: string; noOpen?: boolean; browserPath?: string; userDataDir?: string },
325+
options?: {
326+
projectName?: string;
327+
noOpen?: boolean;
328+
browserPath?: string;
329+
userDataDir?: string;
330+
remoteDebuggingPort?: number;
331+
},
279332
): Promise<void> {
280333
const req = createRequire(join(dir, "package.json"));
281334
const studioPkgPath = dirname(req.resolve("@hyperframes/studio/package.json"));
@@ -327,6 +380,7 @@ async function runLocalStudioMode(
327380
openBrowser(`${url}#project/${pName}`, {
328381
browserPath: options?.browserPath,
329382
userDataDir: options?.userDataDir,
383+
remoteDebuggingPort: options?.remoteDebuggingPort,
330384
});
331385
}
332386
}
@@ -370,6 +424,7 @@ async function runEmbeddedMode(
370424
noOpen?: boolean;
371425
browserPath?: string;
372426
userDataDir?: string;
427+
remoteDebuggingPort?: number;
373428
},
374429
): Promise<void> {
375430
const { createStudioServer, resolveStudioBundle } = await import("../server/studioServer.js");
@@ -424,6 +479,7 @@ async function runEmbeddedMode(
424479
openBrowser(`${url}#project/${pName}`, {
425480
browserPath: options?.browserPath,
426481
userDataDir: options?.userDataDir,
482+
remoteDebuggingPort: options?.remoteDebuggingPort,
427483
});
428484
}
429485
return;
@@ -448,6 +504,7 @@ async function runEmbeddedMode(
448504
openBrowser(`${url}#project/${pName}`, {
449505
browserPath: options?.browserPath,
450506
userDataDir: options?.userDataDir,
507+
remoteDebuggingPort: options?.remoteDebuggingPort,
451508
});
452509
}
453510

packages/cli/src/utils/openBrowser.test.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "vitest";
2-
import { buildBrowserArgs } from "./openBrowser.js";
2+
import { buildBrowserArgs, parseRemoteDebuggingPort } from "./openBrowser.js";
33

44
describe("buildBrowserArgs", () => {
55
it("returns only the URL when no options are given", () => {
@@ -37,4 +37,80 @@ describe("buildBrowserArgs", () => {
3737
}),
3838
).toEqual(["--user-data-dir=C:\\Documents and Settings\\profile", "http://localhost:3002"]);
3939
});
40+
41+
it("prepends --remote-debugging-port before the URL", () => {
42+
expect(
43+
buildBrowserArgs("http://localhost:3002", {
44+
browserPath: "/usr/bin/chromium",
45+
userDataDir: "/tmp/hf-profile",
46+
remoteDebuggingPort: 9222,
47+
}),
48+
).toEqual([
49+
"--user-data-dir=/tmp/hf-profile",
50+
"--remote-debugging-port=9222",
51+
"http://localhost:3002",
52+
]);
53+
});
54+
55+
it("includes all flags together", () => {
56+
expect(
57+
buildBrowserArgs("http://localhost:3002", {
58+
browserPath: "/usr/bin/chromium",
59+
userDataDir: "/tmp/hf-profile",
60+
remoteDebuggingPort: 9222,
61+
}),
62+
).toEqual([
63+
"--user-data-dir=/tmp/hf-profile",
64+
"--remote-debugging-port=9222",
65+
"http://localhost:3002",
66+
]);
67+
});
68+
});
69+
70+
describe("parseRemoteDebuggingPort", () => {
71+
it("returns undefined for undefined", () => {
72+
expect(parseRemoteDebuggingPort(undefined)).toBeUndefined();
73+
});
74+
75+
it("returns undefined for empty string", () => {
76+
expect(parseRemoteDebuggingPort("")).toBeUndefined();
77+
});
78+
79+
it("parses a valid port number", () => {
80+
expect(parseRemoteDebuggingPort("9222")).toBe(9222);
81+
});
82+
83+
it("parses port 1 (minimum)", () => {
84+
expect(parseRemoteDebuggingPort("1")).toBe(1);
85+
});
86+
87+
it("parses port 65535 (maximum)", () => {
88+
expect(parseRemoteDebuggingPort("65535")).toBe(65535);
89+
});
90+
91+
it("rejects 0", () => {
92+
expect(() => parseRemoteDebuggingPort("0")).toThrow(
93+
"--remote-debugging-port must be an integer between 1 and 65535",
94+
);
95+
});
96+
97+
it("rejects negative numbers", () => {
98+
expect(() => parseRemoteDebuggingPort("-1")).toThrow();
99+
});
100+
101+
it("rejects non-numeric input", () => {
102+
expect(() => parseRemoteDebuggingPort("abc")).toThrow();
103+
});
104+
105+
it("rejects trailing non-digits (no parseInt leakage)", () => {
106+
expect(() => parseRemoteDebuggingPort("9222abc")).toThrow();
107+
});
108+
109+
it("rejects numbers above 65535", () => {
110+
expect(() => parseRemoteDebuggingPort("70000")).toThrow();
111+
});
112+
113+
it("rejects decimals", () => {
114+
expect(() => parseRemoteDebuggingPort("22.5")).toThrow();
115+
});
40116
});

packages/cli/src/utils/openBrowser.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@ import { spawn } from "node:child_process";
33
export interface OpenBrowserOptions {
44
browserPath?: string;
55
userDataDir?: string;
6+
remoteDebuggingPort?: number;
7+
}
8+
9+
/**
10+
* Validate and parse a --remote-debugging-port value.
11+
* Returns the port number or undefined if not provided.
12+
* Throws if the value is not a valid integer in 1..65535.
13+
*/
14+
export function parseRemoteDebuggingPort(value: unknown): number | undefined {
15+
if (value === undefined || value === null || value === "") return undefined;
16+
17+
const text = String(value);
18+
19+
if (!/^\d+$/.test(text)) {
20+
throw new Error("--remote-debugging-port must be an integer between 1 and 65535");
21+
}
22+
23+
const port = Number(text);
24+
25+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
26+
throw new Error("--remote-debugging-port must be an integer between 1 and 65535");
27+
}
28+
29+
return port;
630
}
731

832
/**
@@ -15,6 +39,9 @@ export function buildBrowserArgs(url: string, options: OpenBrowserOptions): stri
1539
if (options.userDataDir) {
1640
args.push(`--user-data-dir=${options.userDataDir}`);
1741
}
42+
if (options.remoteDebuggingPort !== undefined) {
43+
args.push(`--remote-debugging-port=${options.remoteDebuggingPort}`);
44+
}
1845
args.push(url);
1946
return args;
2047
}
@@ -24,6 +51,7 @@ export function buildBrowserArgs(url: string, options: OpenBrowserOptions): stri
2451
*
2552
* - browserPath: spawn the given binary directly (enables Chromium flags)
2653
* - userDataDir: passed as --user-data-dir (requires browserPath)
54+
* - remoteDebuggingPort: passed as --remote-debugging-port (requires browserPath + userDataDir)
2755
* - otherwise: fall back to the `open` package (default browser)
2856
*/
2957
export function openBrowser(url: string, options: OpenBrowserOptions = {}): void {

0 commit comments

Comments
 (0)