diff --git a/.changeset/custom-dashboard-port.md b/.changeset/custom-dashboard-port.md new file mode 100644 index 0000000..5d8f6f3 --- /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 009e491..228fd75 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 f7f0ad4..4176cf7 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()).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 9da3bd6..eb89a47 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 @@ -340,10 +344,59 @@ 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; + 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: String(port) }, + }, + }; +} + +/** + * Validate dashboard port option. + * @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 async function dashboard(options: CommandOptions = {}): Promise { +export function validateDashboardPort(port?: number): 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; +} + +/** + * 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); consola.start("Starting dashboard..."); const { configFile } = await loadConfigWithEnv(configPath); @@ -355,10 +408,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 ae49225..fb45bb3 100644 --- a/packages/docs/docs/cli.mdx +++ b/packages/docs/docs/cli.mdx @@ -124,8 +124,26 @@ bunx @openworkflow/cli dashboard -This launches the OpenWorkflow dashboard on `http://localhost:3000`. See -[Dashboard](/docs/dashboard) for details. +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 +``` + + + +See [Dashboard](/docs/dashboard) for details. ### `doctor` diff --git a/packages/docs/docs/dashboard.mdx b/packages/docs/docs/dashboard.mdx index e3f9844..43417f5 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