Skip to content

Commit 4affe2b

Browse files
committed
fix(cli): address PR #983 review feedback
- play.ts: move --remote-debugging-port parse+deps validation before any server setup so an invalid value exits cleanly instead of leaking a listening socket (the original bug — server printed 'Player running' and 'Press Ctrl+C to stop' before failing). - Extract validateRemoteDebuggingPortDeps() in openBrowser.ts to keep preview.ts and play.ts in sync instead of copy-pasting the dep checks. - Narrow parseRemoteDebuggingPort param to string | undefined; drop the dead null branch and the redundant String() / Number.isInteger() now that the regex already constrains the input. - buildBrowserArgs: omit --remote-debugging-port when userDataDir is missing so a CDP endpoint cannot leak into the user's main profile even if a caller bypasses the CLI validation layer. - Replace the duplicated buildBrowserArgs case with one that proves this defense-in-depth behaviour; add unit tests for validateRemoteDebuggingPortDeps. - Drop the heavy JSDoc on parseRemoteDebuggingPort to match the file's surrounding style. - Both commands: align --remote-debugging-port description (it now matches the actual 'requires --browser-path and --user-data-dir' contract) and add a CDP example to the --help output.
1 parent e5e0772 commit 4affe2b

4 files changed

Lines changed: 137 additions & 69 deletions

File tree

packages/cli/src/commands/play.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@ export const examples: Example[] = [
88
["Use a custom port", "hyperframes play --port 8080"],
99
["Start without opening the browser", "hyperframes play --no-open"],
1010
["Open with a specific browser", "hyperframes play --browser-path /usr/bin/chromium"],
11+
[
12+
"Open with CDP enabled (requires browser path + isolated profile)",
13+
"hyperframes play --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile --remote-debugging-port 9222",
14+
],
1115
];
1216
import { resolve, dirname } from "node:path";
1317
import * as clack from "@clack/prompts";
1418
import { c } from "../ui/colors.js";
1519
import { resolveProject } from "../utils/project.js";
16-
import { openBrowser, parseRemoteDebuggingPort } from "../utils/openBrowser.js";
20+
import {
21+
openBrowser,
22+
parseRemoteDebuggingPort,
23+
validateRemoteDebuggingPortDeps,
24+
} from "../utils/openBrowser.js";
1725

1826
export default defineCommand({
1927
meta: { name: "play", description: "Play a composition in a lightweight browser player" },
@@ -48,18 +56,28 @@ export default defineCommand({
4856
process.exitCode = 1;
4957
return;
5058
}
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-
}
59+
// Validation: --remote-debugging-port deps
60+
const depsError = validateRemoteDebuggingPortDeps({
61+
browserPath: args["browser-path"] as string | undefined,
62+
userDataDir: args["user-data-dir"] as string | undefined,
63+
remoteDebuggingPort: args["remote-debugging-port"] as string | undefined,
64+
});
65+
if (depsError) {
66+
clack.log.error(depsError);
67+
process.exitCode = 1;
68+
return;
69+
}
70+
// Parse --remote-debugging-port before any server setup so an invalid value
71+
// exits cleanly instead of leaving an orphan listening socket behind.
72+
let remoteDebuggingPort: number | undefined;
73+
try {
74+
remoteDebuggingPort = parseRemoteDebuggingPort(
75+
args["remote-debugging-port"] as string | undefined,
76+
);
77+
} catch (err) {
78+
clack.log.error((err as Error).message);
79+
process.exitCode = 1;
80+
return;
6381
}
6482

6583
// Resolve runtime path — same logic as studioServer.ts
@@ -185,16 +203,6 @@ export default defineCommand({
185203
console.log();
186204
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
187205
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-
}
198206

199207
if (args.open) {
200208
void openBrowser(url, {

packages/cli/src/commands/preview.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export const examples: Example[] = [
99
["Force a new server even if one is already running", "hyperframes preview --force-new"],
1010
["Start without opening the browser", "hyperframes preview --no-open"],
1111
["Open with a specific browser", "hyperframes preview --browser-path /usr/bin/chromium"],
12+
[
13+
"Open with CDP enabled (requires browser path + isolated profile)",
14+
"hyperframes preview --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile --remote-debugging-port 9222",
15+
],
1216
["List all active preview servers", "hyperframes preview --list"],
1317
["Kill all active preview servers", "hyperframes preview --kill-all"],
1418
];
@@ -19,7 +23,11 @@ import { createRequire } from "node:module";
1923
import * as clack from "@clack/prompts";
2024
import { c } from "../ui/colors.js";
2125
import { isDevMode } from "../utils/env.js";
22-
import { openBrowser, parseRemoteDebuggingPort } from "../utils/openBrowser.js";
26+
import {
27+
openBrowser,
28+
parseRemoteDebuggingPort,
29+
validateRemoteDebuggingPortDeps,
30+
} from "../utils/openBrowser.js";
2331
import { lintProject } from "../utils/lintProject.js";
2432
import { formatLintFindings } from "../utils/lintFormat.js";
2533
import {
@@ -126,32 +134,30 @@ export default defineCommand({
126134
process.exitCode = 1;
127135
return;
128136
}
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-
}
137+
// Validation: --remote-debugging-port deps
138+
const depsError = validateRemoteDebuggingPortDeps({
139+
browserPath: args["browser-path"] as string | undefined,
140+
userDataDir: args["user-data-dir"] as string | undefined,
141+
remoteDebuggingPort: args["remote-debugging-port"] as string | undefined,
142+
});
143+
if (depsError) {
144+
clack.log.error(depsError);
145+
process.exitCode = 1;
146+
return;
141147
}
142148

143149
const noOpen = !args.open;
144150
const browserPath = args["browser-path"] as string | undefined;
145151
const userDataDir = args["user-data-dir"] as string | undefined;
146152
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-
}
153+
try {
154+
remoteDebuggingPort = parseRemoteDebuggingPort(
155+
args["remote-debugging-port"] as string | undefined,
156+
);
157+
} catch (err) {
158+
clack.log.error((err as Error).message);
159+
process.exitCode = 1;
160+
return;
155161
}
156162

157163
if (isDevMode()) {

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

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, it, expect } from "vitest";
2-
import { buildBrowserArgs, parseRemoteDebuggingPort } from "./openBrowser.js";
2+
import {
3+
buildBrowserArgs,
4+
parseRemoteDebuggingPort,
5+
validateRemoteDebuggingPortDeps,
6+
} from "./openBrowser.js";
37

48
describe("buildBrowserArgs", () => {
59
it("returns only the URL when no options are given", () => {
@@ -38,18 +42,16 @@ describe("buildBrowserArgs", () => {
3842
).toEqual(["--user-data-dir=C:\\Documents and Settings\\profile", "http://localhost:3002"]);
3943
});
4044

41-
it("prepends --remote-debugging-port before the URL", () => {
45+
it("omits --remote-debugging-port when userDataDir is missing (defense in depth)", () => {
46+
// The CLI validation layer rejects this combination upstream, but
47+
// buildBrowserArgs must not leak a CDP endpoint into the user's main
48+
// profile even if a caller bypasses that check.
4249
expect(
4350
buildBrowserArgs("http://localhost:3002", {
4451
browserPath: "/usr/bin/chromium",
45-
userDataDir: "/tmp/hf-profile",
4652
remoteDebuggingPort: 9222,
4753
}),
48-
).toEqual([
49-
"--user-data-dir=/tmp/hf-profile",
50-
"--remote-debugging-port=9222",
51-
"http://localhost:3002",
52-
]);
54+
).toEqual(["http://localhost:3002"]);
5355
});
5456

5557
it("includes all flags together", () => {
@@ -114,3 +116,45 @@ describe("parseRemoteDebuggingPort", () => {
114116
expect(() => parseRemoteDebuggingPort("22.5")).toThrow();
115117
});
116118
});
119+
120+
describe("validateRemoteDebuggingPortDeps", () => {
121+
it("returns null when --remote-debugging-port is not set", () => {
122+
expect(validateRemoteDebuggingPortDeps({})).toBeNull();
123+
});
124+
125+
it("returns null when all required flags are present", () => {
126+
expect(
127+
validateRemoteDebuggingPortDeps({
128+
browserPath: "/usr/bin/chromium",
129+
userDataDir: "/tmp/hf-profile",
130+
remoteDebuggingPort: "9222",
131+
}),
132+
).toBeNull();
133+
});
134+
135+
it("requires --browser-path when --remote-debugging-port is set", () => {
136+
expect(
137+
validateRemoteDebuggingPortDeps({
138+
userDataDir: "/tmp/hf-profile",
139+
remoteDebuggingPort: "9222",
140+
}),
141+
).toBe("--remote-debugging-port requires --browser-path");
142+
});
143+
144+
it("requires --user-data-dir when --remote-debugging-port is set", () => {
145+
expect(
146+
validateRemoteDebuggingPortDeps({
147+
browserPath: "/usr/bin/chromium",
148+
remoteDebuggingPort: "9222",
149+
}),
150+
).toBe("--remote-debugging-port requires --user-data-dir");
151+
});
152+
153+
it("reports --browser-path first when both deps are missing", () => {
154+
expect(
155+
validateRemoteDebuggingPortDeps({
156+
remoteDebuggingPort: "9222",
157+
}),
158+
).toBe("--remote-debugging-port requires --browser-path");
159+
});
160+
});

packages/cli/src/utils/openBrowser.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,35 @@ export interface OpenBrowserOptions {
66
remoteDebuggingPort?: number;
77
}
88

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)) {
9+
export function parseRemoteDebuggingPort(value: string | undefined): number | undefined {
10+
if (value === undefined || value === "") return undefined;
11+
if (!/^\d+$/.test(value)) {
2012
throw new Error("--remote-debugging-port must be an integer between 1 and 65535");
2113
}
22-
23-
const port = Number(text);
24-
25-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
14+
const port = Number(value);
15+
if (port < 1 || port > 65535) {
2616
throw new Error("--remote-debugging-port must be an integer between 1 and 65535");
2717
}
28-
2918
return port;
3019
}
3120

21+
export interface RemoteDebuggingPortDeps {
22+
browserPath?: string;
23+
userDataDir?: string;
24+
remoteDebuggingPort?: string;
25+
}
26+
27+
/**
28+
* Returns an error message if --remote-debugging-port is set without its required
29+
* dependencies (--browser-path and --user-data-dir), or null if everything is OK.
30+
*/
31+
export function validateRemoteDebuggingPortDeps(deps: RemoteDebuggingPortDeps): string | null {
32+
if (!deps.remoteDebuggingPort) return null;
33+
if (!deps.browserPath) return "--remote-debugging-port requires --browser-path";
34+
if (!deps.userDataDir) return "--remote-debugging-port requires --user-data-dir";
35+
return null;
36+
}
37+
3238
/**
3339
* Build the argument list for spawning a browser process.
3440
*
@@ -39,7 +45,11 @@ export function buildBrowserArgs(url: string, options: OpenBrowserOptions): stri
3945
if (options.userDataDir) {
4046
args.push(`--user-data-dir=${options.userDataDir}`);
4147
}
42-
if (options.remoteDebuggingPort !== undefined) {
48+
// Defense-in-depth: only emit --remote-debugging-port when paired with an
49+
// isolated --user-data-dir. Without an isolated profile the CDP endpoint
50+
// would expose the user's main browser session, which is the whole reason
51+
// the CLI validation layer requires both flags together.
52+
if (options.remoteDebuggingPort !== undefined && options.userDataDir) {
4353
args.push(`--remote-debugging-port=${options.remoteDebuggingPort}`);
4454
}
4555
args.push(url);

0 commit comments

Comments
 (0)