Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion packages/cli/src/commands/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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,
});
}

Expand Down
67 changes: 62 additions & 5 deletions packages/cli/src/commands/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -62,6 +62,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);
Expand Down Expand Up @@ -122,18 +126,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"];
Expand All @@ -143,6 +182,7 @@ export default defineCommand({
noOpen,
browserPath,
userDataDir,
remoteDebuggingPort,
});
},
});
Expand All @@ -152,7 +192,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);
Expand Down Expand Up @@ -222,6 +268,7 @@ async function runDevMode(
openBrowser(urlToOpen, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}

Expand Down Expand Up @@ -275,7 +322,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"));
Expand Down Expand Up @@ -327,6 +380,7 @@ async function runLocalStudioMode(
openBrowser(`${url}#project/${pName}`, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}
}
Expand Down Expand Up @@ -370,6 +424,7 @@ async function runEmbeddedMode(
noOpen?: boolean;
browserPath?: string;
userDataDir?: string;
remoteDebuggingPort?: number;
},
): Promise<void> {
const { createStudioServer, resolveStudioBundle } = await import("../server/studioServer.js");
Expand Down Expand Up @@ -424,6 +479,7 @@ async function runEmbeddedMode(
openBrowser(`${url}#project/${pName}`, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}
return;
Expand All @@ -448,6 +504,7 @@ async function runEmbeddedMode(
openBrowser(`${url}#project/${pName}`, {
browserPath: options?.browserPath,
userDataDir: options?.userDataDir,
remoteDebuggingPort: options?.remoteDebuggingPort,
});
}

Expand Down
78 changes: 77 additions & 1 deletion packages/cli/src/utils/openBrowser.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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 changes: 28 additions & 0 deletions packages/cli/src/utils/openBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
}
Expand All @@ -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 {
Expand Down
Loading