diff --git a/openworkflow.config.ts b/openworkflow.config.ts new file mode 100644 index 0000000..e29445d --- /dev/null +++ b/openworkflow.config.ts @@ -0,0 +1,12 @@ +import { BackendPostgres } from "@openworkflow/backend-postgres"; +import { OpenWorkflow } from "openworkflow"; + +const backend = await BackendPostgres.connect( + "postgresql://postgres:postgres@localhost:5432/postgres", +); +const ow = new OpenWorkflow({ backend }); + +export default { + ow, + port: 3000, +}; diff --git a/package-lock.json b/package-lock.json index b3fc500..c21a58c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -768,6 +768,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", + "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "dev": true, @@ -885,6 +897,14 @@ "resolved": "packages/backend-postgres", "link": true }, + "node_modules/@openworkflow/cli": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@openworkflow/dashboard": { + "resolved": "packages/dashboard", + "link": true + }, "node_modules/@pkgr/core": { "version": "0.2.9", "dev": true, @@ -1979,6 +1999,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -2650,6 +2679,15 @@ "node": ">=8" } }, + "node_modules/hono": { + "version": "4.10.6", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz", + "integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4192,6 +4230,26 @@ "openworkflow": "^0.3.0" } }, + "packages/cli": { + "name": "@openworkflow/cli", + "version": "0.1.0", + "dependencies": { + "@openworkflow/dashboard": "file:../dashboard", + "commander": "^14.0.2", + "openworkflow": "file:../openworkflow" + }, + "bin": { + "ow": "bin/cli.ts" + } + }, + "packages/dashboard": { + "name": "@openworkflow/dashboard", + "version": "0.1.0", + "dependencies": { + "@hono/node-server": "^1.0.0", + "hono": "^4.0.0" + } + }, "packages/openworkflow": { "version": "0.3.0", "license": "Apache-2.0", diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..fc1124e --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,12 @@ +# CLI + +Proof of concept: + +- Uses tsx +- Only resolves openworkflow.config.ts from project root + +Production: + +- Should compile with tsc +- Robustly resolve config +- Use esbuild internally to compile and then load openworkflow.config.ts diff --git a/packages/cli/bin/cli.ts b/packages/cli/bin/cli.ts new file mode 100755 index 0000000..1a78a7f --- /dev/null +++ b/packages/cli/bin/cli.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env -S tsx + +import { serveCommand } from "../serve.js"; +import { Command } from "commander"; + +const program = new Command(); + +program + .name("ow") + .description("OpenWorkflow CLI") + .version("0.1.0") + .addCommand(serveCommand) + .parse(); diff --git a/packages/cli/config.ts b/packages/cli/config.ts new file mode 100644 index 0000000..c07c745 --- /dev/null +++ b/packages/cli/config.ts @@ -0,0 +1,13 @@ +import path from "node:path"; +import { OpenWorkflow } from "openworkflow"; + +export async function resolveConfig(): Promise { + const configPath = path.resolve(process.cwd(), "openworkflow.config.ts"); + const config = (await import(configPath)) as { default: Config }; + return config.default; +} + +export interface Config { + ow: OpenWorkflow; + port: number; +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..93f1089 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,13 @@ +{ + "name": "@openworkflow/cli", + "version": "0.1.0", + "type": "module", + "bin": { + "ow": "./bin/cli.ts" + }, + "dependencies": { + "@openworkflow/dashboard": "file:../dashboard", + "commander": "^14.0.2", + "openworkflow": "file:../openworkflow" + } +} diff --git a/packages/cli/serve.ts b/packages/cli/serve.ts new file mode 100644 index 0000000..7650383 --- /dev/null +++ b/packages/cli/serve.ts @@ -0,0 +1,9 @@ +import { resolveConfig } from "./config.js"; +import { Command } from "commander"; + +export const serveCommand = new Command("serve") + .description("Start the OpenWorkflow server") + .action(async () => { + const { ow, port } = await resolveConfig(); + await ow.serve({ port }); + }); diff --git a/packages/dashboard/README.md b/packages/dashboard/README.md new file mode 100644 index 0000000..18556b5 --- /dev/null +++ b/packages/dashboard/README.md @@ -0,0 +1,3 @@ +# Dashboard + +Frontend framework tbd, it should build index.html, etc to dist/ diff --git a/packages/dashboard/index.js b/packages/dashboard/index.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json new file mode 100644 index 0000000..d17528f --- /dev/null +++ b/packages/dashboard/package.json @@ -0,0 +1,13 @@ +{ + "name": "@openworkflow/dashboard", + "version": "0.1.0", + "type": "module", + "main": "index.js", + "files": [ + "dist" + ], + "dependencies": { + "@hono/node-server": "^1.0.0", + "hono": "^4.0.0" + } +} diff --git a/packages/openworkflow/client.ts b/packages/openworkflow/client.ts index 7ede418..e8b4f0b 100644 --- a/packages/openworkflow/client.ts +++ b/packages/openworkflow/client.ts @@ -97,6 +97,21 @@ export class OpenWorkflow { return definition; } + + async serve(options?: { port?: number }): Promise { + let observability: typeof import("./observability.js"); + + try { + observability = await import("./observability.js"); + } catch { + throw new Error( + "Install @openworkflow/dashboard to enable observability:\n\n" + + "npm install @openworkflow/dashboard\n", + ); + } + + observability.serve({ ow: this, port: options?.port ?? 3000 }); + } } // diff --git a/packages/openworkflow/observability.ts b/packages/openworkflow/observability.ts new file mode 100644 index 0000000..d28b695 --- /dev/null +++ b/packages/openworkflow/observability.ts @@ -0,0 +1,38 @@ +import { OpenWorkflow } from "./index.js"; +import { serve as serveNode } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { Hono } from "hono"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export function serve(options: ObservabilityOptions) { + const port = options.port ?? 3000; + const app = new Hono(); + + const dashboardPath = path.dirname( + fileURLToPath( + import.meta.resolve("@openworkflow/dashboard", import.meta.url), + ), + ); + + // API endpoints are a slim HTTP wrapper around the OpenWorkflow client + app.get("/api/test", (c) => { + return c.json({ + message: "Fetched from openworkflow observability server", + }); + }); + + // Serve the dashboard SPA + app.get("/", serveStatic({ root: path.resolve(dashboardPath, "dist") })); + + serveNode({ fetch: app.fetch, port }); + + console.info( + `OpenWorkflow dashboard running on http://localhost:${port.toString()}/`, + ); +} + +export interface ObservabilityOptions { + ow: OpenWorkflow; + port?: number; +}