feat(cli): add --no-open flag to preview and play (#871)

Add --no-open boolean flag to both commands via citty's built-in
boolean negation (--no-open sets args.open to false).

- preview.ts: guard all 4 open() calls with args.open check
- play.ts: guard the open() call with args.open check
- Default is true (open browser), preserving existing behavior

Closes #1

Co-authored-by: AnoKno <122017492+AnoKno@users.noreply.github.com>
This commit is contained in:
na-navi
2026-05-15 21:03:37 +02:00
committed by GitHub
co-authored by AnoKno
parent 4bc24068f4
commit adeb92ecb7
2 changed files with 46 additions and 17 deletions
+9 -1
View File
@@ -6,6 +6,7 @@ export const examples: Example[] = [
["Play the current project", "hyperframes play"],
["Play a specific project directory", "hyperframes play ./my-video"],
["Use a custom port", "hyperframes play --port 8080"],
["Start without opening the browser", "hyperframes play --no-open"],
];
import { resolve, dirname } from "node:path";
import * as clack from "@clack/prompts";
@@ -17,6 +18,11 @@ export default defineCommand({
args: {
dir: { type: "positional", description: "Project directory", required: false },
port: { type: "string", description: "Port to run the player server on", default: "3003" },
open: {
type: "boolean",
default: true,
description: "Open browser automatically",
},
},
async run({ args }) {
const project = resolveProject(args.dir);
@@ -145,7 +151,9 @@ export default defineCommand({
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
import("open").then((mod) => mod.default(url)).catch(() => {});
if (args.open) {
import("open").then((mod) => mod.default(url)).catch(() => {});
}
return new Promise<void>(() => {});
},
+37 -16
View File
@@ -7,6 +7,7 @@ export const examples: Example[] = [
["Preview a specific project directory", "hyperframes preview ./my-video"],
["Use a custom port", "hyperframes preview --port 8080"],
["Force a new server even if one is already running", "hyperframes preview --force-new"],
["Start without opening the browser", "hyperframes preview --no-open"],
["List all active preview servers", "hyperframes preview --list"],
["Kill all active preview servers", "hyperframes preview --kill-all"],
];
@@ -46,6 +47,11 @@ export default defineCommand({
description: "Kill all active preview servers and exit",
default: false,
},
open: {
type: "boolean",
default: true,
description: "Open browser automatically",
},
},
async run({ args }) {
const startPort = parseInt(args.port ?? "3002", 10);
@@ -100,31 +106,36 @@ export default defineCommand({
}
}
const noOpen = !args.open;
if (isDevMode()) {
return runDevMode(dir, projectName);
return runDevMode(dir, { projectName, noOpen });
}
// If @hyperframes/studio is installed locally, use Vite for full HMR
if (hasLocalStudio(dir)) {
return runLocalStudioMode(dir, projectName);
return runLocalStudioMode(dir, { projectName, noOpen });
}
const forceNew = !!args["force-new"];
return runEmbeddedMode(dir, startPort, projectName, forceNew);
return runEmbeddedMode(dir, startPort, { projectName, forceNew, noOpen });
},
});
/**
* Dev mode: spawn the studio dev server from the monorepo.
*/
async function runDevMode(dir: string, projectName?: string): Promise<void> {
async function runDevMode(
dir: string,
options?: { projectName?: string; noOpen?: boolean },
): Promise<void> {
// Find monorepo root by navigating from packages/cli/src/commands/
const thisFile = fileURLToPath(import.meta.url);
const repoRoot = resolve(dirname(thisFile), "..", "..", "..", "..");
// Symlink project into the studio's data directory
const projectsDir = join(repoRoot, "packages", "studio", "data", "projects");
const pName = projectName ?? basename(dir);
const pName = options?.projectName ?? basename(dir);
const symlinkPath = join(projectsDir, pName);
mkdirSync(projectsDir, { recursive: true });
@@ -181,8 +192,10 @@ async function runDevMode(dir: string, projectName?: string): Promise<void> {
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
const urlToOpen = `${frontendUrl}#project/${pName}`;
import("open").then((mod) => mod.default(urlToOpen)).catch(() => {});
if (!options?.noOpen) {
const urlToOpen = `${frontendUrl}#project/${pName}`;
import("open").then((mod) => mod.default(urlToOpen)).catch(() => {});
}
child.stdout?.removeListener("data", handleOutput);
child.stderr?.removeListener("data", handleOutput);
@@ -232,10 +245,13 @@ function hasLocalStudio(dir: string): boolean {
* Local studio mode: spawn Vite using a locally installed @hyperframes/studio.
* Provides full Vite HMR and the complete studio experience.
*/
async function runLocalStudioMode(dir: string, projectName?: string): Promise<void> {
async function runLocalStudioMode(
dir: string,
options?: { projectName?: string; noOpen?: boolean },
): Promise<void> {
const req = createRequire(join(dir, "package.json"));
const studioPkgPath = dirname(req.resolve("@hyperframes/studio/package.json"));
const pName = projectName ?? basename(dir);
const pName = options?.projectName ?? basename(dir);
// Symlink project into studio's data directory
const projectsDir = join(studioPkgPath, "data", "projects");
@@ -279,7 +295,9 @@ async function runLocalStudioMode(dir: string, projectName?: string): Promise<vo
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
if (!options?.noOpen) {
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
}
}
}
@@ -315,12 +333,11 @@ async function runLocalStudioMode(dir: string, projectName?: string): Promise<vo
async function runEmbeddedMode(
dir: string,
startPort: number,
projectName?: string,
forceNew = false,
options?: { projectName?: string; forceNew?: boolean; noOpen?: boolean },
): Promise<void> {
const { createStudioServer, resolveStudioBundle } = await import("../server/studioServer.js");
const pName = projectName ?? basename(dir);
const pName = options?.projectName ?? basename(dir);
const studioBundle = resolveStudioBundle();
clack.intro(c.bold("hyperframes preview"));
@@ -345,7 +362,7 @@ async function runEmbeddedMode(
let result: FindPortResult;
try {
result = await findPortAndServe(app.fetch, startPort, dir, forceNew);
result = await findPortAndServe(app.fetch, startPort, dir, !!options?.forceNew);
} catch (err: unknown) {
s.stop(c.error("Failed to start studio"));
console.error();
@@ -366,7 +383,9 @@ async function runEmbeddedMode(
` ${c.dim("Reusing existing server. Use --force-new to start a fresh instance.")}`,
);
console.log();
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
if (!options?.noOpen) {
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
}
return;
}
@@ -385,7 +404,9 @@ async function runEmbeddedMode(
console.log();
console.log(` ${c.dim("Press Ctrl+C to stop")}`);
console.log();
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
if (!options?.noOpen) {
import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {});
}
// Block until Ctrl+C. Node would normally exit on SIGINT, but the listening
// HTTP server keeps handles open, so the event loop stays alive after the