From 106b56a49a365b0f19f0c6b8a5a75585bf03953d Mon Sep 17 00:00:00 2001 From: James Martinez Date: Tue, 20 Jan 2026 20:49:32 -0600 Subject: [PATCH 1/5] Spawn dashboard process from cli with `openworkflow dashboard` --- packages/cli/cli.ts | 14 +++++++- packages/cli/commands.ts | 59 +++++++++++++++++++++++++++++++++ packages/dashboard/package.json | 4 +++ turbo.json | 2 +- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/cli/cli.ts b/packages/cli/cli.ts index 8cdae7e..479e23c 100644 --- a/packages/cli/cli.ts +++ b/packages/cli/cli.ts @@ -1,6 +1,12 @@ #!/usr/bin/env node /* v8 ignore file -- @preserve */ -import { doctor, getVersion, init, workerStart } from "./commands.js"; +import { + dashboard, + doctor, + getVersion, + init, + workerStart, +} from "./commands.js"; import { withErrorHandling } from "./errors.js"; import { Command } from "commander"; @@ -38,4 +44,10 @@ workerCmd ) .action(withErrorHandling(workerStart)); +// dashboard +program + .command("dashboard") + .description("start the dashboard to view workflow runs") + .action(withErrorHandling(dashboard)); + await program.parseAsync(process.argv); diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index c2e13f8..3a75e09 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -10,6 +10,7 @@ import * as p from "@clack/prompts"; import { consola } from "consola"; import { config as loadDotenv } from "dotenv"; import { createJiti } from "jiti"; +import { spawn } from "node:child_process"; import { existsSync, mkdirSync, @@ -308,6 +309,64 @@ export async function workerStart(cliOptions: WorkerConfig): Promise { } } +/** + * openworkflow dashboard + * Starts the dashboard by delegating to `@openworkflow/dashboard` via npx. + */ +export async function dashboard(): Promise { + consola.start("Starting dashboard..."); + + const { configFile } = await loadConfigWithEnv(); + if (!configFile) { + throw new CLIError( + "No config file found.", + "Run `ow init` to create a config file before starting the dashboard.", + ); + } + consola.info(`Using config: ${configFile}`); + + // eslint-disable-next-line sonarjs/no-os-command-from-path + const child = spawn("npx", ["@openworkflow/dashboard"], { + stdio: "inherit", + shell: true, + env: { ...process.env }, + }); + + await new Promise((resolve, reject) => { + child.on("error", (error) => { + reject( + new CLIError( + "Failed to start dashboard.", + `Could not spawn npx: ${error.message}`, + ), + ); + }); + + child.on("exit", (code) => { + if (code === 0) { + resolve(); + } else { + reject( + new CLIError( + "Dashboard exited with an error.", + `Exit code: ${String(code)}`, + ), + ); + } + }); + + /** + * Graceful shutdown on signals. + * @param signal - Signal + */ + function signalHandler(signal: NodeJS.Signals): void { + child.kill(signal); + } + process.on("SIGINT", signalHandler); + process.on("SIGTERM", signalHandler); + }); +} + // ----------------------------------------------------------------------------- /** diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 5117e04..52e805a 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -2,10 +2,14 @@ "name": "@openworkflow/dashboard", "private": true, "type": "module", + "files": [ + ".output" + ], "scripts": { "build": "vite build", "dev": "vite dev --port 3000", "preview": "vite preview", + "start": "node ./.output/server/index.mjs", "test": "vitest run", "typecheck": "tsc --noEmit" }, diff --git a/turbo.json b/turbo.json index 78434fa..87f27ef 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,7 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist/**"] + "outputs": ["dist/**", ".output/**"] } } } From 7c108bfd5440e73bcc381495d80c18f796ff0520 Mon Sep 17 00:00:00 2001 From: James Martinez Date: Tue, 20 Jan 2026 23:50:18 -0600 Subject: [PATCH 2/5] Remove signal handlers after the child exits --- packages/cli/commands.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index 3a75e09..4ef5e77 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -333,7 +333,14 @@ export async function dashboard(): Promise { }); await new Promise((resolve, reject) => { + /** remove signal handlers after the child exits */ + function cleanupSignalHandlers(): void { + process.off("SIGINT", signalHandler); + process.off("SIGTERM", signalHandler); + } + child.on("error", (error) => { + cleanupSignalHandlers(); reject( new CLIError( "Failed to start dashboard.", @@ -343,6 +350,7 @@ export async function dashboard(): Promise { }); child.on("exit", (code) => { + cleanupSignalHandlers(); if (code === 0) { resolve(); } else { From ba5405a9443d99284fac97c336ed206b2aff060a Mon Sep 17 00:00:00 2001 From: James Martinez Date: Tue, 20 Jan 2026 23:50:47 -0600 Subject: [PATCH 3/5] Remove double env --- packages/cli/commands.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index 4ef5e77..736c396 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -329,7 +329,6 @@ export async function dashboard(): Promise { const child = spawn("npx", ["@openworkflow/dashboard"], { stdio: "inherit", shell: true, - env: { ...process.env }, }); await new Promise((resolve, reject) => { From 9efeb7cedf4e61db4951537ea702ed7cff190c15 Mon Sep 17 00:00:00 2001 From: James Martinez Date: Tue, 20 Jan 2026 23:52:47 -0600 Subject: [PATCH 4/5] Resolve dashboard process on null exit code --- packages/cli/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index 736c396..b66c2a1 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -350,7 +350,7 @@ export async function dashboard(): Promise { child.on("exit", (code) => { cleanupSignalHandlers(); - if (code === 0) { + if (code === 0 || code === null) { resolve(); } else { reject( From ea8f98df5f55a0e0a8c9e2f6c104de9d078008ad Mon Sep 17 00:00:00 2001 From: James Martinez Date: Wed, 21 Jan 2026 00:29:46 -0600 Subject: [PATCH 5/5] Dashboard bin for npx --- package-lock.json | 3 +++ packages/cli/commands.ts | 1 - packages/dashboard/bin.mjs | 10 ++++++++++ packages/dashboard/package.json | 4 +++- 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100755 packages/dashboard/bin.mjs diff --git a/package-lock.json b/package-lock.json index 1079eff..31a0478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15568,6 +15568,9 @@ "vite-tsconfig-paths": "^5.1.4", "zod": "^4.2.1" }, + "bin": { + "dashboard": "bin.mjs" + }, "devDependencies": { "@tanstack/devtools-vite": "^0.3.11", "@testing-library/dom": "^10.4.0", diff --git a/packages/cli/commands.ts b/packages/cli/commands.ts index b66c2a1..964f009 100644 --- a/packages/cli/commands.ts +++ b/packages/cli/commands.ts @@ -328,7 +328,6 @@ export async function dashboard(): Promise { // eslint-disable-next-line sonarjs/no-os-command-from-path const child = spawn("npx", ["@openworkflow/dashboard"], { stdio: "inherit", - shell: true, }); await new Promise((resolve, reject) => { diff --git a/packages/dashboard/bin.mjs b/packages/dashboard/bin.mjs new file mode 100755 index 0000000..c8da1dc --- /dev/null +++ b/packages/dashboard/bin.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// simple wrapper to load the dashboard server since the index.mjs file does not +// have a shebang line +const __dirname = dirname(fileURLToPath(import.meta.url)); +const serverPath = join(__dirname, ".output", "server", "index.mjs"); + +await import(serverPath); diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 52e805a..744f9b8 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -2,8 +2,10 @@ "name": "@openworkflow/dashboard", "private": true, "type": "module", + "bin": "./bin.mjs", "files": [ - ".output" + ".output", + "bin.mjs" ], "scripts": { "build": "vite build",