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.
This commit is contained in:
AnoKno
2026-05-25 15:38:41 -04:00
committed by Miguel Ángel
parent 28f948aa7d
commit 3902a9a82b
4 changed files with 197 additions and 7 deletions
+30 -1
View File
@@ -13,7 +13,7 @@ import { resolve, dirname } from "node:path";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
import { resolveProject } from "../utils/project.js";
import { openBrowser } from "../utils/openBrowser.js";
import { openBrowser, parseRemoteDebuggingPort } from "../utils/openBrowser.js";
export default defineCommand({
meta: { name: "play", description: "Play a composition in a lightweight browser player" },
@@ -33,6 +33,10 @@ export default defineCommand({
type: "string",
description: "Chromium-compatible user data directory (requires --browser-path)",
},
"remote-debugging-port": {
type: "string",
description: "Chromium remote debugging port (requires --browser-path and --user-data-dir)",
},
},
async run({ args }) {
const project = resolveProject(args.dir);
@@ -44,6 +48,19 @@ export default defineCommand({
process.exitCode = 1;
return;
}
// Validation: --remote-debugging-port requires --browser-path and --user-data-dir
if (args["remote-debugging-port"]) {
if (!args["browser-path"]) {
clack.log.error("--remote-debugging-port requires --browser-path");
process.exitCode = 1;
return;
}
if (!args["user-data-dir"]) {
clack.log.error("--remote-debugging-port requires --user-data-dir");
process.exitCode = 1;
return;
}
}
// Resolve runtime path — same logic as studioServer.ts
const runtimePath = resolveRuntimePath();
@@ -168,10 +185,22 @@ export default defineCommand({
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
let remoteDebuggingPort: number | undefined;
if (args["remote-debugging-port"]) {
try {
remoteDebuggingPort = parseRemoteDebuggingPort(args["remote-debugging-port"]);
} catch (err) {
clack.log.error((err as Error).message);
process.exitCode = 1;
return;
}
}
if (args.open) {
void openBrowser(url, {
browserPath: args["browser-path"] as string | undefined,
userDataDir: args["user-data-dir"] as string | undefined,
remoteDebuggingPort,
});
}
+62 -5
View File
@@ -19,7 +19,7 @@ import { createRequire } from "node:module";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
import { isDevMode } from "../utils/env.js";
import { openBrowser } from "../utils/openBrowser.js";
import { openBrowser, parseRemoteDebuggingPort } from "../utils/openBrowser.js";
import { lintProject } from "../utils/lintProject.js";
import { formatLintFindings } from "../utils/lintFormat.js";
import {
@@ -63,6 +63,10 @@ export default defineCommand({
type: "string",
description: "Chromium-compatible user data directory (requires --browser-path)",
},
"remote-debugging-port": {
type: "string",
description: "Chromium remote debugging port (requires --browser-path and --user-data-dir)",
},
},
async run({ args }) {
const startPort = parseInt(args.port ?? "3002", 10);
@@ -131,18 +135,53 @@ export default defineCommand({
process.exitCode = 1;
return;
}
// Validation: --remote-debugging-port requires --browser-path and --user-data-dir
if (args["remote-debugging-port"]) {
if (!args["browser-path"]) {
clack.log.error("--remote-debugging-port requires --browser-path");
process.exitCode = 1;
return;
}
if (!args["user-data-dir"]) {
clack.log.error("--remote-debugging-port requires --user-data-dir");
process.exitCode = 1;
return;
}
}
const noOpen = !args.open;
const browserPath = args["browser-path"] as string | undefined;
const userDataDir = args["user-data-dir"] as string | undefined;
let remoteDebuggingPort: number | undefined;
if (args["remote-debugging-port"]) {
try {
remoteDebuggingPort = parseRemoteDebuggingPort(args["remote-debugging-port"]);
} catch (err) {
clack.log.error((err as Error).message);
process.exitCode = 1;
return;
}
}
if (isDevMode()) {
return runDevMode(dir, { projectName, noOpen, browserPath, userDataDir });
return runDevMode(dir, {
projectName,
noOpen,
browserPath,
userDataDir,
remoteDebuggingPort,
});
}
// If @hyperframes/studio is installed locally, use Vite for full HMR
if (hasLocalStudio(dir)) {
return runLocalStudioMode(dir, { projectName, noOpen, browserPath, userDataDir });
return runLocalStudioMode(dir, {
projectName,
noOpen,
browserPath,
userDataDir,
remoteDebuggingPort,
});
}
const forceNew = !!args["force-new"];
@@ -152,6 +191,7 @@ export default defineCommand({
noOpen,
browserPath,
userDataDir,
remoteDebuggingPort,
});
},
});
@@ -161,7 +201,13 @@ export default defineCommand({
*/
async function runDevMode(
dir: string,
options?: { projectName?: string; noOpen?: boolean; browserPath?: string; userDataDir?: string },
options?: {
projectName?: string;
noOpen?: boolean;
browserPath?: string;
userDataDir?: string;
remoteDebuggingPort?: number;
},
): Promise<void> {
// Find monorepo root by navigating from packages/cli/src/commands/
const thisFile = fileURLToPath(import.meta.url);
@@ -231,6 +277,7 @@ async function runDevMode(
openBrowser(urlToOpen, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}
@@ -294,7 +341,13 @@ function hasLocalStudio(dir: string): boolean {
*/
async function runLocalStudioMode(
dir: string,
options?: { projectName?: string; noOpen?: boolean; browserPath?: string; userDataDir?: string },
options?: {
projectName?: string;
noOpen?: boolean;
browserPath?: string;
userDataDir?: string;
remoteDebuggingPort?: number;
},
): Promise<void> {
const req = createRequire(join(dir, "package.json"));
const studioPkgPath = dirname(req.resolve("@hyperframes/studio/package.json"));
@@ -346,6 +399,7 @@ async function runLocalStudioMode(
openBrowser(`${url}#project/${pName}`, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}
}
@@ -396,6 +450,7 @@ async function runEmbeddedMode(
noOpen?: boolean;
browserPath?: string;
userDataDir?: string;
remoteDebuggingPort?: number;
},
): Promise<void> {
const { createStudioServer, loadPreviewServerBuildSignature, resolveStudioBundle } =
@@ -458,6 +513,7 @@ async function runEmbeddedMode(
openBrowser(`${url}#project/${pName}`, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}
return;
@@ -482,6 +538,7 @@ async function runEmbeddedMode(
openBrowser(`${url}#project/${pName}`, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}
+77 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { buildBrowserArgs } from "./openBrowser.js";
import { buildBrowserArgs, parseRemoteDebuggingPort } from "./openBrowser.js";
describe("buildBrowserArgs", () => {
it("returns only the URL when no options are given", () => {
@@ -37,4 +37,80 @@ describe("buildBrowserArgs", () => {
}),
).toEqual(["--user-data-dir=C:\\Documents and Settings\\profile", "http://localhost:3002"]);
});
it("prepends --remote-debugging-port before the URL", () => {
expect(
buildBrowserArgs("http://localhost:3002", {
browserPath: "/usr/bin/chromium",
userDataDir: "/tmp/hf-profile",
remoteDebuggingPort: 9222,
}),
).toEqual([
"--user-data-dir=/tmp/hf-profile",
"--remote-debugging-port=9222",
"http://localhost:3002",
]);
});
it("includes all flags together", () => {
expect(
buildBrowserArgs("http://localhost:3002", {
browserPath: "/usr/bin/chromium",
userDataDir: "/tmp/hf-profile",
remoteDebuggingPort: 9222,
}),
).toEqual([
"--user-data-dir=/tmp/hf-profile",
"--remote-debugging-port=9222",
"http://localhost:3002",
]);
});
});
describe("parseRemoteDebuggingPort", () => {
it("returns undefined for undefined", () => {
expect(parseRemoteDebuggingPort(undefined)).toBeUndefined();
});
it("returns undefined for empty string", () => {
expect(parseRemoteDebuggingPort("")).toBeUndefined();
});
it("parses a valid port number", () => {
expect(parseRemoteDebuggingPort("9222")).toBe(9222);
});
it("parses port 1 (minimum)", () => {
expect(parseRemoteDebuggingPort("1")).toBe(1);
});
it("parses port 65535 (maximum)", () => {
expect(parseRemoteDebuggingPort("65535")).toBe(65535);
});
it("rejects 0", () => {
expect(() => parseRemoteDebuggingPort("0")).toThrow(
"--remote-debugging-port must be an integer between 1 and 65535",
);
});
it("rejects negative numbers", () => {
expect(() => parseRemoteDebuggingPort("-1")).toThrow();
});
it("rejects non-numeric input", () => {
expect(() => parseRemoteDebuggingPort("abc")).toThrow();
});
it("rejects trailing non-digits (no parseInt leakage)", () => {
expect(() => parseRemoteDebuggingPort("9222abc")).toThrow();
});
it("rejects numbers above 65535", () => {
expect(() => parseRemoteDebuggingPort("70000")).toThrow();
});
it("rejects decimals", () => {
expect(() => parseRemoteDebuggingPort("22.5")).toThrow();
});
});
+28
View File
@@ -3,6 +3,30 @@ import { spawn } from "node:child_process";
export interface OpenBrowserOptions {
browserPath?: string;
userDataDir?: string;
remoteDebuggingPort?: number;
}
/**
* Validate and parse a --remote-debugging-port value.
* Returns the port number or undefined if not provided.
* Throws if the value is not a valid integer in 1..65535.
*/
export function parseRemoteDebuggingPort(value: unknown): number | undefined {
if (value === undefined || value === null || value === "") return undefined;
const text = String(value);
if (!/^\d+$/.test(text)) {
throw new Error("--remote-debugging-port must be an integer between 1 and 65535");
}
const port = Number(text);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("--remote-debugging-port must be an integer between 1 and 65535");
}
return port;
}
/**
@@ -15,6 +39,9 @@ export function buildBrowserArgs(url: string, options: OpenBrowserOptions): stri
if (options.userDataDir) {
args.push(`--user-data-dir=${options.userDataDir}`);
}
if (options.remoteDebuggingPort !== undefined) {
args.push(`--remote-debugging-port=${options.remoteDebuggingPort}`);
}
args.push(url);
return args;
}
@@ -24,6 +51,7 @@ export function buildBrowserArgs(url: string, options: OpenBrowserOptions): stri
*
* - browserPath: spawn the given binary directly (enables Chromium flags)
* - userDataDir: passed as --user-data-dir (requires browserPath)
* - remoteDebuggingPort: passed as --remote-debugging-port (requires browserPath + userDataDir)
* - otherwise: fall back to the `open` package (default browser)
*/
export function openBrowser(url: string, options: OpenBrowserOptions = {}): void {