From 257ab8940e4d83821c161b10c953fff65dc46dea Mon Sep 17 00:00:00 2001 From: srpvpn Date: Mon, 2 Mar 2026 21:28:31 +0300 Subject: [PATCH 1/3] feat(cli): add custom dashboard port option --- .changeset/custom-dashboard-port.md | 5 +++ packages/cli/cli.ts | 5 +++ packages/cli/commands.test.ts | 48 +++++++++++++++++++++++++ packages/cli/commands.ts | 56 ++++++++++++++++++++++++++--- packages/docs/docs/cli.mdx | 20 ++++++++++- packages/docs/docs/dashboard.mdx | 19 +++++++++- 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 .changeset/custom-dashboard-port.md diff --git a/.changeset/custom-dashboard-port.md b/.changeset/custom-dashboard-port.md new file mode 100644 index 00000000..5d8f6f3b --- /dev/null +++ b/.changeset/custom-dashboard-port.md @@ -0,0 +1,5 @@ +--- +"@openworkflow/cli": patch +--- + +Add a `--port` option to `openworkflow dashboard` so users can run the dashboard server on a custom port. diff --git a/packages/cli/cli.ts b/packages/cli/cli.ts index 009e491f..228fd759 100644 --- a/packages/cli/cli.ts +++ b/packages/cli/cli.ts @@ -51,6 +51,11 @@ workerCmd program .command("dashboard") .description("start the dashboard to view workflow runs") + .option( + "-p, --port ", + "custom port for the dashboard server", + Number.parseInt, + ) .option("--config ", "path to OpenWorkflow config file") .action(withErrorHandling(dashboard)); diff --git a/packages/cli/commands.test.ts b/packages/cli/commands.test.ts index f7f0ad43..c1687747 100644 --- a/packages/cli/commands.test.ts +++ b/packages/cli/commands.test.ts @@ -1,9 +1,11 @@ import { discoverWorkflowFiles, + getDashboardSpawnOptions, getClientFileName, getConfigFileName, getExampleWorkflowFileName, getRunFileName, + validateDashboardPort, } from "./commands.js"; import fs from "node:fs"; import os from "node:os"; @@ -141,3 +143,49 @@ describe("discoverWorkflowFiles", () => { } }); }); + +describe("getDashboardSpawnOptions", () => { + test("uses default npx command without a custom port env", () => { + const options = getDashboardSpawnOptions(); + + expect(options.command).toBe("npx"); + expect(options.args).toEqual(["@openworkflow/dashboard"]); + expect(options.spawnOptions.env?.PORT).toBeUndefined(); + expect(options.spawnOptions.stdio).toBe("inherit"); + }); + + test("sets PORT env when a custom dashboard port is provided", () => { + const options = getDashboardSpawnOptions(4321); + + expect(options.command).toBe("npx"); + expect(options.args).toEqual(["@openworkflow/dashboard"]); + expect(options.spawnOptions.env?.PORT).toBe("4321"); + expect(options.spawnOptions.stdio).toBe("inherit"); + }); +}); + +describe("validateDashboardPort", () => { + test("returns undefined when no custom port is provided", () => { + expect(validateDashboardPort(undefined)).toBeUndefined(); + }); + + test("returns the port when it is within range", () => { + expect(validateDashboardPort(3001)).toBe(3001); + }); + + test("throws for non-integer ports", () => { + expect(() => validateDashboardPort(Number.NaN)).toThrow( + "Invalid dashboard port.", + ); + expect(() => validateDashboardPort(3000.5)).toThrow( + "Invalid dashboard port.", + ); + }); + + test("throws for out-of-range ports", () => { + expect(() => validateDashboardPort(0)).toThrow("Invalid dashboard port."); + expect(() => validateDashboardPort(65_536)).toThrow( + "Invalid dashboard port.", + ); + }); +}); diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index 9da3bd65..9fe79107 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -35,6 +35,10 @@ interface CommandOptions { config?: string; } +interface DashboardOptions extends CommandOptions { + port?: number; +} + /** * openworkflow -V | --version * @returns the version string, or "-" if it cannot be determined @@ -342,8 +346,49 @@ export async function workerStart( * Starts the dashboard by delegating to `@openworkflow/dashboard` via npx. * @param options - Command options */ -export async function dashboard(options: CommandOptions = {}): Promise { +export function getDashboardSpawnOptions(port?: number): { + command: string; + args: string[]; + spawnOptions: { + stdio: "inherit"; + env?: NodeJS.ProcessEnv; + }; +} { + return { + command: "npx", + args: ["@openworkflow/dashboard"], + spawnOptions: { + stdio: "inherit", + env: port === undefined ? process.env : { ...process.env, PORT: `${port}` }, + }, + }; +} + +/** + * Validate dashboard port option. + * @param port - Optional dashboard port + * @returns Validated port + */ +export function validateDashboardPort( + port: number | undefined, +): number | undefined { + if (port === undefined) { + return undefined; + } + + if (!Number.isInteger(port) || port < 1 || port > 65_535) { + throw new CLIError( + "Invalid dashboard port.", + "Use an integer between 1 and 65535, for example `--port 3001`.", + ); + } + + return port; +} + +export async function dashboard(options: DashboardOptions = {}): Promise { const configPath = options.config; + const port = validateDashboardPort(options.port); consola.start("Starting dashboard..."); const { configFile } = await loadConfigWithEnv(configPath); @@ -356,9 +401,12 @@ export async function dashboard(options: CommandOptions = {}): Promise { consola.info(`Using config: ${configFile}`); // eslint-disable-next-line sonarjs/no-os-command-from-path - const child = spawn("npx", ["@openworkflow/dashboard"], { - stdio: "inherit", - }); + const spawnConfig = getDashboardSpawnOptions(port); + const child = spawn( + spawnConfig.command, + spawnConfig.args, + spawnConfig.spawnOptions, + ); await new Promise((resolve, reject) => { /** remove signal handlers after the child exits */ diff --git a/packages/docs/docs/cli.mdx b/packages/docs/docs/cli.mdx index ae49225e..207a536d 100644 --- a/packages/docs/docs/cli.mdx +++ b/packages/docs/docs/cli.mdx @@ -124,7 +124,25 @@ bunx @openworkflow/cli dashboard -This launches the OpenWorkflow dashboard on `http://localhost:3000`. See +Use `--port` to run the dashboard on a custom port: + + +```bash npm +npx @openworkflow/cli dashboard --port 4000 +``` + +```bash pnpm +pnpx @openworkflow/cli dashboard --port 4000 +``` + +```bash bun +bunx @openworkflow/cli dashboard --port 4000 +``` + + + +Without `--port`, this launches the OpenWorkflow dashboard on +`http://localhost:3000`. See [Dashboard](/docs/dashboard) for details. ### `doctor` diff --git a/packages/docs/docs/dashboard.mdx b/packages/docs/docs/dashboard.mdx index e3f9844a..43417f5f 100644 --- a/packages/docs/docs/dashboard.mdx +++ b/packages/docs/docs/dashboard.mdx @@ -24,7 +24,24 @@ bunx @openworkflow/cli dashboard -The dashboard starts on `http://localhost:3000`. +The dashboard starts on `http://localhost:3000` by default. + +To use a custom port: + + +```bash npm +npx @openworkflow/cli dashboard --port 4000 +``` + +```bash pnpm +pnpx @openworkflow/cli dashboard --port 4000 +``` + +```bash bun +bunx @openworkflow/cli dashboard --port 4000 +``` + + ## Requirements From b5b75a0d7acc32d84a794c5569e10255266e2d53 Mon Sep 17 00:00:00 2001 From: srpvpn Date: Wed, 4 Mar 2026 21:31:44 +0300 Subject: [PATCH 2/3] refactor(cli): improve dashboard port handling and update tests --- packages/cli/commands.test.ts | 6 +++--- packages/cli/commands.ts | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/cli/commands.test.ts b/packages/cli/commands.test.ts index c1687747..4176cf70 100644 --- a/packages/cli/commands.test.ts +++ b/packages/cli/commands.test.ts @@ -150,7 +150,7 @@ describe("getDashboardSpawnOptions", () => { expect(options.command).toBe("npx"); expect(options.args).toEqual(["@openworkflow/dashboard"]); - expect(options.spawnOptions.env?.PORT).toBeUndefined(); + expect(options.spawnOptions.env?.["PORT"]).toBeUndefined(); expect(options.spawnOptions.stdio).toBe("inherit"); }); @@ -159,14 +159,14 @@ describe("getDashboardSpawnOptions", () => { expect(options.command).toBe("npx"); expect(options.args).toEqual(["@openworkflow/dashboard"]); - expect(options.spawnOptions.env?.PORT).toBe("4321"); + expect(options.spawnOptions.env?.["PORT"]).toBe("4321"); expect(options.spawnOptions.stdio).toBe("inherit"); }); }); describe("validateDashboardPort", () => { test("returns undefined when no custom port is provided", () => { - expect(validateDashboardPort(undefined)).toBeUndefined(); + expect(validateDashboardPort()).toBeUndefined(); }); test("returns the port when it is within range", () => { diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index 9fe79107..eb89a472 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -344,7 +344,8 @@ export async function workerStart( /** * openworkflow dashboard * Starts the dashboard by delegating to `@openworkflow/dashboard` via npx. - * @param options - Command options + * @param port - Optional dashboard port. + * @returns Spawn configuration for launching the dashboard process. */ export function getDashboardSpawnOptions(port?: number): { command: string; @@ -359,19 +360,21 @@ export function getDashboardSpawnOptions(port?: number): { args: ["@openworkflow/dashboard"], spawnOptions: { stdio: "inherit", - env: port === undefined ? process.env : { ...process.env, PORT: `${port}` }, + env: + port === undefined + ? process.env + : { ...process.env, PORT: String(port) }, }, }; } /** * Validate dashboard port option. - * @param port - Optional dashboard port - * @returns Validated port + * @param port - Optional dashboard port. + * @returns Validated dashboard port. + * @throws {CLIError} If the provided port is not an integer in the 1-65535 range. */ -export function validateDashboardPort( - port: number | undefined, -): number | undefined { +export function validateDashboardPort(port?: number): number | undefined { if (port === undefined) { return undefined; } @@ -386,6 +389,11 @@ export function validateDashboardPort( return port; } +/** + * Start the dashboard process. + * @param options - Dashboard command options. + * @returns Resolves when the dashboard process exits. + */ export async function dashboard(options: DashboardOptions = {}): Promise { const configPath = options.config; const port = validateDashboardPort(options.port); @@ -400,7 +408,6 @@ export async function dashboard(options: DashboardOptions = {}): Promise { } consola.info(`Using config: ${configFile}`); - // eslint-disable-next-line sonarjs/no-os-command-from-path const spawnConfig = getDashboardSpawnOptions(port); const child = spawn( spawnConfig.command, From 86ab4b46aee56d81cd166d11ff8d0643a8be5a28 Mon Sep 17 00:00:00 2001 From: srpvpn Date: Wed, 4 Mar 2026 21:41:32 +0300 Subject: [PATCH 3/3] docs(cli): clarify dashboard default URL and port usage --- packages/docs/docs/cli.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs/docs/cli.mdx b/packages/docs/docs/cli.mdx index 207a536d..fb45bb3e 100644 --- a/packages/docs/docs/cli.mdx +++ b/packages/docs/docs/cli.mdx @@ -124,7 +124,9 @@ bunx @openworkflow/cli dashboard -Use `--port` to run the dashboard on a custom port: +The dashboard starts on `http://localhost:3000` by default. + +To use a custom port: ```bash npm @@ -141,9 +143,7 @@ bunx @openworkflow/cli dashboard --port 4000 -Without `--port`, this launches the OpenWorkflow dashboard on -`http://localhost:3000`. See -[Dashboard](/docs/dashboard) for details. +See [Dashboard](/docs/dashboard) for details. ### `doctor`