Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion packages/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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);
65 changes: 65 additions & 0 deletions packages/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -308,6 +309,70 @@ export async function workerStart(cliOptions: WorkerConfig): Promise<void> {
}
}

/**
* openworkflow dashboard
* Starts the dashboard by delegating to `@openworkflow/dashboard` via npx.
*/
export async function dashboard(): Promise<void> {
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",
});

await new Promise<void>((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.",
`Could not spawn npx: ${error.message}`,
),
);
});

child.on("exit", (code) => {
cleanupSignalHandlers();
if (code === 0 || code === null) {
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);
Comment on lines +368 to +372
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signal handler function is defined inside the Promise callback but referenced in the cleanup function before it's defined. While JavaScript hoisting makes this work, it creates a subtle dependency. Consider defining signalHandler before using it in the event listeners, or restructure the code to avoid this forward reference pattern for better clarity.

Copilot uses AI. Check for mistakes.
});
}
Comment on lines +316 to +374
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dashboard() function lacks test coverage. Given that the CLI has test infrastructure (commands.test.ts exists) and the custom coding guideline requires appropriate tests for all new code, tests should be added for the dashboard command. At minimum, test cases should cover: successful dashboard launch, handling of missing config file, and error handling for spawn failures.

Copilot generated this review using guidance from repository custom instructions.

// -----------------------------------------------------------------------------

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/dashboard/bin.mjs
Original file line number Diff line number Diff line change
@@ -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);
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bin wrapper imports the server file but doesn't handle potential import errors. If the .output/server/index.mjs file doesn't exist (e.g., if the dashboard hasn't been built yet), this will crash with an unhelpful error message. Consider adding error handling to provide a clear message directing users to build the dashboard first.

Suggested change
await import(serverPath);
try {
await import(serverPath);
} catch (error) {
// Provide a clearer message if the dashboard server entrypoint is missing.
if (error && (error.code === "ERR_MODULE_NOT_FOUND" || error.code === "MODULE_NOT_FOUND")) {
console.error(
`Dashboard server entrypoint not found at ${serverPath}.` +
" Have you built the dashboard yet? Please run the dashboard build command (for example, `npm run build` in the dashboard package) and try again."
);
} else {
console.error(`Failed to start dashboard server from ${serverPath}.`);
console.error(error);
}
process.exitCode = 1;
}

Copilot uses AI. Check for mistakes.
6 changes: 6 additions & 0 deletions packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
"name": "@openworkflow/dashboard",
"private": true,
"type": "module",
"bin": "./bin.mjs",
"files": [
".output",
"bin.mjs"
],
"scripts": {
"build": "vite build",
"dev": "vite dev --port 3000",
"preview": "vite preview",
"start": "node ./.output/server/index.mjs",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
Expand Down
2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
"outputs": ["dist/**", ".output/**"]
}
}
}
Loading