chisel-studio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +215 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ui/assets/index-B2dja40T.js +2607 -0
- package/dist/ui/assets/index-UB4FWL5j.css +1 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +14 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# chisel-studio
|
|
2
|
+
|
|
3
|
+
Embedded dev UI for the [Chisel](https://github.com/shivkanthb/chisel) workflow engine. A real-time dashboard for monitoring and managing your workflows — no setup, no external services.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Real-time activity feed** — live SSE stream of workflow and step events
|
|
8
|
+
- **Step trace visualization** — see step durations, status, and execution order
|
|
9
|
+
- **Workflow management** — trigger, retry, and cancel runs from the UI
|
|
10
|
+
- **Run details** — inspect input data, step results, errors, and progress
|
|
11
|
+
- **Light & dark mode** — system preference detection with manual toggle
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install chisel-studio
|
|
17
|
+
# or
|
|
18
|
+
pnpm add chisel-studio
|
|
19
|
+
# or
|
|
20
|
+
bun add chisel-studio
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`chisel-engine` and `hono` are peer dependencies.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createEngine } from "chisel-engine";
|
|
29
|
+
import { createStudio } from "chisel-studio";
|
|
30
|
+
|
|
31
|
+
const engine = createEngine({
|
|
32
|
+
connection: { host: "localhost", port: 6379 },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ... register workflows, start engine ...
|
|
36
|
+
|
|
37
|
+
const studio = createStudio(engine, { port: 4040 });
|
|
38
|
+
await studio.start();
|
|
39
|
+
// → Chisel Studio running at http://localhost:4040
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
createStudio(engine, {
|
|
46
|
+
port: 4040, // default: 4040
|
|
47
|
+
host: "localhost", // default: "localhost"
|
|
48
|
+
open: true, // auto-open in browser (default: false)
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
`createStudio()` returns a `StudioServer` object:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const studio = createStudio(engine, options);
|
|
58
|
+
|
|
59
|
+
studio.url; // "http://localhost:4040"
|
|
60
|
+
await studio.start(); // start the server
|
|
61
|
+
await studio.stop(); // stop the server
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## REST Endpoints
|
|
65
|
+
|
|
66
|
+
The studio server exposes these API endpoints:
|
|
67
|
+
|
|
68
|
+
| Method | Endpoint | Description |
|
|
69
|
+
|--------|----------|-------------|
|
|
70
|
+
| `GET` | `/api/health` | Engine health status |
|
|
71
|
+
| `GET` | `/api/workflows` | List registered workflows |
|
|
72
|
+
| `GET` | `/api/workflows/:id/runs` | List runs (supports `limit`, `cursor`, `status` params) |
|
|
73
|
+
| `GET` | `/api/runs/:runId` | Get run details with step breakdown |
|
|
74
|
+
| `POST` | `/api/workflows/:id/trigger` | Trigger a new run |
|
|
75
|
+
| `POST` | `/api/runs/:runId/cancel` | Cancel a running workflow |
|
|
76
|
+
| `POST` | `/api/runs/:runId/retry` | Retry a failed run |
|
|
77
|
+
| `GET` | `/api/events` | SSE stream of real-time events |
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Engine } from 'chisel-engine';
|
|
2
|
+
|
|
3
|
+
interface StudioOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
host?: string;
|
|
6
|
+
open?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface StudioServer {
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
readonly url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare function createStudio(engine: Engine, options?: StudioOptions): StudioServer;
|
|
15
|
+
|
|
16
|
+
export { type StudioOptions, type StudioServer, createStudio };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Engine } from 'chisel-engine';
|
|
2
|
+
|
|
3
|
+
interface StudioOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
host?: string;
|
|
6
|
+
open?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface StudioServer {
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
readonly url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare function createStudio(engine: Engine, options?: StudioOptions): StudioServer;
|
|
15
|
+
|
|
16
|
+
export { type StudioOptions, type StudioServer, createStudio };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var nodeServer = require('@hono/node-server');
|
|
4
|
+
var hono = require('hono');
|
|
5
|
+
var cors = require('hono/cors');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var streaming = require('hono/streaming');
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
function createApiRoutes(engine) {
|
|
12
|
+
const app = new hono.Hono();
|
|
13
|
+
app.get("/health", async (c) => {
|
|
14
|
+
const health = await engine.health();
|
|
15
|
+
return c.json(health, health.connected ? 200 : 503);
|
|
16
|
+
});
|
|
17
|
+
app.get("/workflows", (c) => {
|
|
18
|
+
return c.json(engine.listWorkflows());
|
|
19
|
+
});
|
|
20
|
+
app.get("/workflows/:id/runs", async (c) => {
|
|
21
|
+
const id = c.req.param("id");
|
|
22
|
+
const query = c.req.query();
|
|
23
|
+
const result = await engine.listRuns(id, {
|
|
24
|
+
limit: query.limit ? Number(query.limit) : void 0,
|
|
25
|
+
cursor: query.cursor ? Number(query.cursor) : void 0,
|
|
26
|
+
order: query.order,
|
|
27
|
+
status: query.status
|
|
28
|
+
});
|
|
29
|
+
return c.json(result);
|
|
30
|
+
});
|
|
31
|
+
app.get("/runs/:runId", async (c) => {
|
|
32
|
+
const runId = c.req.param("runId");
|
|
33
|
+
const run = await engine.getRun(runId);
|
|
34
|
+
if (!run) {
|
|
35
|
+
return c.json({ error: "Run not found" }, 404);
|
|
36
|
+
}
|
|
37
|
+
return c.json(run);
|
|
38
|
+
});
|
|
39
|
+
app.post("/runs/:runId/cancel", async (c) => {
|
|
40
|
+
const runId = c.req.param("runId");
|
|
41
|
+
try {
|
|
42
|
+
await engine.cancelRun(runId);
|
|
43
|
+
return c.json({ success: true });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return c.json(
|
|
46
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
47
|
+
400
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
app.post("/runs/:runId/retry", async (c) => {
|
|
52
|
+
const runId = c.req.param("runId");
|
|
53
|
+
try {
|
|
54
|
+
await engine.retryRun(runId);
|
|
55
|
+
return c.json({ success: true });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return c.json(
|
|
58
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
59
|
+
400
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
app.post("/workflows/:id/trigger", async (c) => {
|
|
64
|
+
const id = c.req.param("id");
|
|
65
|
+
try {
|
|
66
|
+
const body = await c.req.json();
|
|
67
|
+
const { runId } = await engine.trigger(id, body);
|
|
68
|
+
return c.json({ runId }, 202);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return c.json(
|
|
71
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
72
|
+
400
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return app;
|
|
77
|
+
}
|
|
78
|
+
var ENGINE_EVENTS = [
|
|
79
|
+
"workflow:start",
|
|
80
|
+
"workflow:complete",
|
|
81
|
+
"workflow:fail",
|
|
82
|
+
"step:start",
|
|
83
|
+
"step:complete",
|
|
84
|
+
"step:fail",
|
|
85
|
+
"step:retry"
|
|
86
|
+
];
|
|
87
|
+
function createSseRoute(engine) {
|
|
88
|
+
const app = new hono.Hono();
|
|
89
|
+
app.get("/events", (c) => {
|
|
90
|
+
return streaming.streamSSE(c, async (stream) => {
|
|
91
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const event of ENGINE_EVENTS) {
|
|
93
|
+
const handler = (payload) => {
|
|
94
|
+
stream.writeSSE({
|
|
95
|
+
event,
|
|
96
|
+
data: JSON.stringify(
|
|
97
|
+
payload,
|
|
98
|
+
(_key, value) => value instanceof Error ? { message: value.message, name: value.name } : value
|
|
99
|
+
)
|
|
100
|
+
}).catch(() => {
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
handlers.set(event, handler);
|
|
104
|
+
engine.on(event, handler);
|
|
105
|
+
}
|
|
106
|
+
const heartbeat = setInterval(() => {
|
|
107
|
+
stream.writeSSE({
|
|
108
|
+
event: "heartbeat",
|
|
109
|
+
data: JSON.stringify({ time: Date.now() })
|
|
110
|
+
}).catch(() => {
|
|
111
|
+
});
|
|
112
|
+
}, 15e3);
|
|
113
|
+
try {
|
|
114
|
+
while (true) {
|
|
115
|
+
await stream.sleep(1e3);
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
clearInterval(heartbeat);
|
|
119
|
+
for (const [event, handler] of handlers) {
|
|
120
|
+
engine.off(event, handler);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
return app;
|
|
126
|
+
}
|
|
127
|
+
var MIME_TYPES = {
|
|
128
|
+
".html": "text/html; charset=utf-8",
|
|
129
|
+
".css": "text/css; charset=utf-8",
|
|
130
|
+
".js": "application/javascript; charset=utf-8",
|
|
131
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
132
|
+
".json": "application/json; charset=utf-8",
|
|
133
|
+
".svg": "image/svg+xml",
|
|
134
|
+
".png": "image/png",
|
|
135
|
+
".ico": "image/x-icon",
|
|
136
|
+
".woff": "font/woff",
|
|
137
|
+
".woff2": "font/woff2"
|
|
138
|
+
};
|
|
139
|
+
function createStaticHandler(uiDir) {
|
|
140
|
+
const indexPath = path.join(uiDir, "index.html");
|
|
141
|
+
let indexHtml = "";
|
|
142
|
+
if (fs.existsSync(indexPath)) {
|
|
143
|
+
indexHtml = fs.readFileSync(indexPath, "utf-8");
|
|
144
|
+
}
|
|
145
|
+
return async (c) => {
|
|
146
|
+
const reqPath = new URL(c.req.url).pathname;
|
|
147
|
+
const filePath = path.join(uiDir, reqPath);
|
|
148
|
+
if (fs.existsSync(filePath) && reqPath !== "/") {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(filePath);
|
|
151
|
+
const ext = path.extname(filePath);
|
|
152
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
153
|
+
return new Response(content, {
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": contentType,
|
|
156
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!indexHtml) {
|
|
163
|
+
return c.text("Studio UI not built. Run: bun run build:ui", 500);
|
|
164
|
+
}
|
|
165
|
+
return c.html(indexHtml);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/server.ts
|
|
170
|
+
function resolveUiDir() {
|
|
171
|
+
const fromDist = path.join(__dirname, "ui");
|
|
172
|
+
if (fs.existsSync(path.join(fromDist, "index.html"))) return fromDist;
|
|
173
|
+
const fromSrc = path.join(__dirname, "..", "dist", "ui");
|
|
174
|
+
if (fs.existsSync(path.join(fromSrc, "index.html"))) return fromSrc;
|
|
175
|
+
return fromDist;
|
|
176
|
+
}
|
|
177
|
+
function createStudioApp(engine) {
|
|
178
|
+
const app = new hono.Hono();
|
|
179
|
+
app.use("*", cors.cors());
|
|
180
|
+
app.route("/api", createApiRoutes(engine));
|
|
181
|
+
app.route("/api", createSseRoute(engine));
|
|
182
|
+
app.get("*", createStaticHandler(resolveUiDir()));
|
|
183
|
+
return app;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/index.ts
|
|
187
|
+
function createStudio(engine, options = {}) {
|
|
188
|
+
const port = options.port ?? 4040;
|
|
189
|
+
const host = options.host ?? "localhost";
|
|
190
|
+
const url = `http://${host}:${port}`;
|
|
191
|
+
const app = createStudioApp(engine);
|
|
192
|
+
let server = null;
|
|
193
|
+
return {
|
|
194
|
+
get url() {
|
|
195
|
+
return url;
|
|
196
|
+
},
|
|
197
|
+
async start() {
|
|
198
|
+
server = nodeServer.serve({ fetch: app.fetch, port, hostname: host });
|
|
199
|
+
console.log(`Chisel Studio running at ${url}`);
|
|
200
|
+
if (options.open) {
|
|
201
|
+
const { exec } = await import('child_process');
|
|
202
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
203
|
+
exec(`${cmd} ${url}`);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
async stop() {
|
|
207
|
+
if (server) {
|
|
208
|
+
server.close();
|
|
209
|
+
server = null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
exports.createStudio = createStudio;
|
|
216
|
+
//# sourceMappingURL=index.js.map
|
|
217
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/routes/api.ts","../src/routes/sse.ts","../src/static.ts","../src/server.ts","../src/index.ts"],"names":["Hono","streamSSE","join","existsSync","readFileSync","extname","cors","serve"],"mappings":"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","file":"index.js","sourcesContent":["import { Hono } from \"hono\";\nimport type { Engine } from \"chisel-engine\";\nimport type { RunStatus } from \"chisel-engine\";\n\nexport function createApiRoutes(engine: Engine): Hono {\n const app = new Hono();\n\n // Health check\n app.get(\"/health\", async (c) => {\n const health = await engine.health();\n return c.json(health, health.connected ? 200 : 503);\n });\n\n // List registered workflows\n app.get(\"/workflows\", (c) => {\n return c.json(engine.listWorkflows());\n });\n\n // List runs for a workflow\n app.get(\"/workflows/:id/runs\", async (c) => {\n const id = c.req.param(\"id\");\n const query = c.req.query();\n\n const result = await engine.listRuns(id, {\n limit: query.limit ? Number(query.limit) : undefined,\n cursor: query.cursor ? Number(query.cursor) : undefined,\n order: query.order as \"asc\" | \"desc\" | undefined,\n status: query.status as RunStatus | undefined,\n });\n\n return c.json(result);\n });\n\n // Get run detail\n app.get(\"/runs/:runId\", async (c) => {\n const runId = c.req.param(\"runId\");\n const run = await engine.getRun(runId);\n\n if (!run) {\n return c.json({ error: \"Run not found\" }, 404);\n }\n\n return c.json(run);\n });\n\n // Cancel a run\n app.post(\"/runs/:runId/cancel\", async (c) => {\n const runId = c.req.param(\"runId\");\n\n try {\n await engine.cancelRun(runId);\n return c.json({ success: true });\n } catch (error) {\n return c.json(\n { error: error instanceof Error ? error.message : String(error) },\n 400\n );\n }\n });\n\n // Retry a failed run\n app.post(\"/runs/:runId/retry\", async (c) => {\n const runId = c.req.param(\"runId\");\n\n try {\n await engine.retryRun(runId);\n return c.json({ success: true });\n } catch (error) {\n return c.json(\n { error: error instanceof Error ? error.message : String(error) },\n 400\n );\n }\n });\n\n // Trigger a workflow\n app.post(\"/workflows/:id/trigger\", async (c) => {\n const id = c.req.param(\"id\");\n\n try {\n const body = await c.req.json();\n const { runId } = await engine.trigger(id, body);\n return c.json({ runId }, 202);\n } catch (error) {\n return c.json(\n { error: error instanceof Error ? error.message : String(error) },\n 400\n );\n }\n });\n\n return app;\n}\n","import { Hono } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { Engine, EngineEventName } from \"chisel-engine\";\n\nconst ENGINE_EVENTS: EngineEventName[] = [\n \"workflow:start\",\n \"workflow:complete\",\n \"workflow:fail\",\n \"step:start\",\n \"step:complete\",\n \"step:fail\",\n \"step:retry\",\n];\n\nexport function createSseRoute(engine: Engine): Hono {\n const app = new Hono();\n\n app.get(\"/events\", (c) => {\n return streamSSE(c, async (stream) => {\n const handlers = new Map<string, (payload: unknown) => void>();\n\n for (const event of ENGINE_EVENTS) {\n const handler = (payload: unknown) => {\n stream\n .writeSSE({\n event,\n data: JSON.stringify(payload, (_key, value) =>\n value instanceof Error\n ? { message: value.message, name: value.name }\n : value\n ),\n })\n .catch(() => {});\n };\n handlers.set(event, handler);\n engine.on(event, handler as any);\n }\n\n // Heartbeat every 15 seconds\n const heartbeat = setInterval(() => {\n stream\n .writeSSE({\n event: \"heartbeat\",\n data: JSON.stringify({ time: Date.now() }),\n })\n .catch(() => {});\n }, 15_000);\n\n // Keep the stream alive until client disconnects\n try {\n while (true) {\n await stream.sleep(1000);\n }\n } finally {\n clearInterval(heartbeat);\n for (const [event, handler] of handlers) {\n engine.off(event as EngineEventName, handler as any);\n }\n }\n });\n });\n\n return app;\n}\n","import { readFileSync, existsSync } from \"fs\";\nimport { join, extname } from \"path\";\nimport type { Context } from \"hono\";\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"application/javascript; charset=utf-8\",\n \".mjs\": \"application/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nexport function createStaticHandler(uiDir: string) {\n // Pre-load index.html into memory (small file, hot path)\n const indexPath = join(uiDir, \"index.html\");\n let indexHtml = \"\";\n if (existsSync(indexPath)) {\n indexHtml = readFileSync(indexPath, \"utf-8\");\n }\n\n return async (c: Context) => {\n const reqPath = new URL(c.req.url).pathname;\n\n // Try serving static asset\n const filePath = join(uiDir, reqPath);\n if (existsSync(filePath) && reqPath !== \"/\") {\n try {\n const content = readFileSync(filePath);\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || \"application/octet-stream\";\n return new Response(content, {\n headers: {\n \"Content-Type\": contentType,\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n },\n });\n } catch {\n // Fall through to index.html\n }\n }\n\n // SPA fallback: serve index.html for all non-file routes\n if (!indexHtml) {\n return c.text(\"Studio UI not built. Run: bun run build:ui\", 500);\n }\n return c.html(indexHtml);\n };\n}\n","import { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { join } from \"path\";\nimport { existsSync } from \"fs\";\nimport type { Engine } from \"chisel-engine\";\nimport { createApiRoutes } from \"./routes/api\";\nimport { createSseRoute } from \"./routes/sse\";\nimport { createStaticHandler } from \"./static\";\n\nfunction resolveUiDir(): string {\n // When running from built output: __dirname is dist/, ui is dist/ui/\n const fromDist = join(__dirname, \"ui\");\n if (existsSync(join(fromDist, \"index.html\"))) return fromDist;\n\n // When running from source via bun/tsx: __dirname is src/, ui is ../dist/ui/\n const fromSrc = join(__dirname, \"..\", \"dist\", \"ui\");\n if (existsSync(join(fromSrc, \"index.html\"))) return fromSrc;\n\n // Fallback\n return fromDist;\n}\n\nexport function createStudioApp(engine: Engine): Hono {\n const app = new Hono();\n\n // Enable CORS for development\n app.use(\"*\", cors());\n\n // API routes\n app.route(\"/api\", createApiRoutes(engine));\n\n // SSE events\n app.route(\"/api\", createSseRoute(engine));\n\n // Static SPA assets\n app.get(\"*\", createStaticHandler(resolveUiDir()));\n\n return app;\n}\n","import { serve } from \"@hono/node-server\";\nimport type { Engine } from \"chisel-engine\";\nimport { createStudioApp } from \"./server\";\nimport type { StudioOptions, StudioServer } from \"./types\";\n\nexport type { StudioOptions, StudioServer } from \"./types\";\n\nexport function createStudio(\n engine: Engine,\n options: StudioOptions = {}\n): StudioServer {\n const port = options.port ?? 4040;\n const host = options.host ?? \"localhost\";\n const url = `http://${host}:${port}`;\n\n const app = createStudioApp(engine);\n let server: ReturnType<typeof serve> | null = null;\n\n return {\n get url() {\n return url;\n },\n\n async start() {\n server = serve({ fetch: app.fetch, port, hostname: host });\n\n console.log(`Chisel Studio running at ${url}`);\n\n if (options.open) {\n const { exec } = await import(\"child_process\");\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${cmd} ${url}`);\n }\n },\n\n async stop() {\n if (server) {\n server.close();\n server = null;\n }\n },\n };\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { cors } from 'hono/cors';
|
|
4
|
+
import { join, extname } from 'path';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { streamSSE } from 'hono/streaming';
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
function createApiRoutes(engine) {
|
|
10
|
+
const app = new Hono();
|
|
11
|
+
app.get("/health", async (c) => {
|
|
12
|
+
const health = await engine.health();
|
|
13
|
+
return c.json(health, health.connected ? 200 : 503);
|
|
14
|
+
});
|
|
15
|
+
app.get("/workflows", (c) => {
|
|
16
|
+
return c.json(engine.listWorkflows());
|
|
17
|
+
});
|
|
18
|
+
app.get("/workflows/:id/runs", async (c) => {
|
|
19
|
+
const id = c.req.param("id");
|
|
20
|
+
const query = c.req.query();
|
|
21
|
+
const result = await engine.listRuns(id, {
|
|
22
|
+
limit: query.limit ? Number(query.limit) : void 0,
|
|
23
|
+
cursor: query.cursor ? Number(query.cursor) : void 0,
|
|
24
|
+
order: query.order,
|
|
25
|
+
status: query.status
|
|
26
|
+
});
|
|
27
|
+
return c.json(result);
|
|
28
|
+
});
|
|
29
|
+
app.get("/runs/:runId", async (c) => {
|
|
30
|
+
const runId = c.req.param("runId");
|
|
31
|
+
const run = await engine.getRun(runId);
|
|
32
|
+
if (!run) {
|
|
33
|
+
return c.json({ error: "Run not found" }, 404);
|
|
34
|
+
}
|
|
35
|
+
return c.json(run);
|
|
36
|
+
});
|
|
37
|
+
app.post("/runs/:runId/cancel", async (c) => {
|
|
38
|
+
const runId = c.req.param("runId");
|
|
39
|
+
try {
|
|
40
|
+
await engine.cancelRun(runId);
|
|
41
|
+
return c.json({ success: true });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return c.json(
|
|
44
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
45
|
+
400
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
app.post("/runs/:runId/retry", async (c) => {
|
|
50
|
+
const runId = c.req.param("runId");
|
|
51
|
+
try {
|
|
52
|
+
await engine.retryRun(runId);
|
|
53
|
+
return c.json({ success: true });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return c.json(
|
|
56
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
57
|
+
400
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
app.post("/workflows/:id/trigger", async (c) => {
|
|
62
|
+
const id = c.req.param("id");
|
|
63
|
+
try {
|
|
64
|
+
const body = await c.req.json();
|
|
65
|
+
const { runId } = await engine.trigger(id, body);
|
|
66
|
+
return c.json({ runId }, 202);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return c.json(
|
|
69
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
70
|
+
400
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return app;
|
|
75
|
+
}
|
|
76
|
+
var ENGINE_EVENTS = [
|
|
77
|
+
"workflow:start",
|
|
78
|
+
"workflow:complete",
|
|
79
|
+
"workflow:fail",
|
|
80
|
+
"step:start",
|
|
81
|
+
"step:complete",
|
|
82
|
+
"step:fail",
|
|
83
|
+
"step:retry"
|
|
84
|
+
];
|
|
85
|
+
function createSseRoute(engine) {
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
app.get("/events", (c) => {
|
|
88
|
+
return streamSSE(c, async (stream) => {
|
|
89
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const event of ENGINE_EVENTS) {
|
|
91
|
+
const handler = (payload) => {
|
|
92
|
+
stream.writeSSE({
|
|
93
|
+
event,
|
|
94
|
+
data: JSON.stringify(
|
|
95
|
+
payload,
|
|
96
|
+
(_key, value) => value instanceof Error ? { message: value.message, name: value.name } : value
|
|
97
|
+
)
|
|
98
|
+
}).catch(() => {
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
handlers.set(event, handler);
|
|
102
|
+
engine.on(event, handler);
|
|
103
|
+
}
|
|
104
|
+
const heartbeat = setInterval(() => {
|
|
105
|
+
stream.writeSSE({
|
|
106
|
+
event: "heartbeat",
|
|
107
|
+
data: JSON.stringify({ time: Date.now() })
|
|
108
|
+
}).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
}, 15e3);
|
|
111
|
+
try {
|
|
112
|
+
while (true) {
|
|
113
|
+
await stream.sleep(1e3);
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
clearInterval(heartbeat);
|
|
117
|
+
for (const [event, handler] of handlers) {
|
|
118
|
+
engine.off(event, handler);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return app;
|
|
124
|
+
}
|
|
125
|
+
var MIME_TYPES = {
|
|
126
|
+
".html": "text/html; charset=utf-8",
|
|
127
|
+
".css": "text/css; charset=utf-8",
|
|
128
|
+
".js": "application/javascript; charset=utf-8",
|
|
129
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
130
|
+
".json": "application/json; charset=utf-8",
|
|
131
|
+
".svg": "image/svg+xml",
|
|
132
|
+
".png": "image/png",
|
|
133
|
+
".ico": "image/x-icon",
|
|
134
|
+
".woff": "font/woff",
|
|
135
|
+
".woff2": "font/woff2"
|
|
136
|
+
};
|
|
137
|
+
function createStaticHandler(uiDir) {
|
|
138
|
+
const indexPath = join(uiDir, "index.html");
|
|
139
|
+
let indexHtml = "";
|
|
140
|
+
if (existsSync(indexPath)) {
|
|
141
|
+
indexHtml = readFileSync(indexPath, "utf-8");
|
|
142
|
+
}
|
|
143
|
+
return async (c) => {
|
|
144
|
+
const reqPath = new URL(c.req.url).pathname;
|
|
145
|
+
const filePath = join(uiDir, reqPath);
|
|
146
|
+
if (existsSync(filePath) && reqPath !== "/") {
|
|
147
|
+
try {
|
|
148
|
+
const content = readFileSync(filePath);
|
|
149
|
+
const ext = extname(filePath);
|
|
150
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
151
|
+
return new Response(content, {
|
|
152
|
+
headers: {
|
|
153
|
+
"Content-Type": contentType,
|
|
154
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!indexHtml) {
|
|
161
|
+
return c.text("Studio UI not built. Run: bun run build:ui", 500);
|
|
162
|
+
}
|
|
163
|
+
return c.html(indexHtml);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/server.ts
|
|
168
|
+
function resolveUiDir() {
|
|
169
|
+
const fromDist = join(__dirname, "ui");
|
|
170
|
+
if (existsSync(join(fromDist, "index.html"))) return fromDist;
|
|
171
|
+
const fromSrc = join(__dirname, "..", "dist", "ui");
|
|
172
|
+
if (existsSync(join(fromSrc, "index.html"))) return fromSrc;
|
|
173
|
+
return fromDist;
|
|
174
|
+
}
|
|
175
|
+
function createStudioApp(engine) {
|
|
176
|
+
const app = new Hono();
|
|
177
|
+
app.use("*", cors());
|
|
178
|
+
app.route("/api", createApiRoutes(engine));
|
|
179
|
+
app.route("/api", createSseRoute(engine));
|
|
180
|
+
app.get("*", createStaticHandler(resolveUiDir()));
|
|
181
|
+
return app;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/index.ts
|
|
185
|
+
function createStudio(engine, options = {}) {
|
|
186
|
+
const port = options.port ?? 4040;
|
|
187
|
+
const host = options.host ?? "localhost";
|
|
188
|
+
const url = `http://${host}:${port}`;
|
|
189
|
+
const app = createStudioApp(engine);
|
|
190
|
+
let server = null;
|
|
191
|
+
return {
|
|
192
|
+
get url() {
|
|
193
|
+
return url;
|
|
194
|
+
},
|
|
195
|
+
async start() {
|
|
196
|
+
server = serve({ fetch: app.fetch, port, hostname: host });
|
|
197
|
+
console.log(`Chisel Studio running at ${url}`);
|
|
198
|
+
if (options.open) {
|
|
199
|
+
const { exec } = await import('child_process');
|
|
200
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
201
|
+
exec(`${cmd} ${url}`);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
async stop() {
|
|
205
|
+
if (server) {
|
|
206
|
+
server.close();
|
|
207
|
+
server = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { createStudio };
|
|
214
|
+
//# sourceMappingURL=index.mjs.map
|
|
215
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/routes/api.ts","../src/routes/sse.ts","../src/static.ts","../src/server.ts","../src/index.ts"],"names":["Hono","join","existsSync"],"mappings":"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","file":"index.mjs","sourcesContent":["import { Hono } from \"hono\";\nimport type { Engine } from \"chisel-engine\";\nimport type { RunStatus } from \"chisel-engine\";\n\nexport function createApiRoutes(engine: Engine): Hono {\n const app = new Hono();\n\n // Health check\n app.get(\"/health\", async (c) => {\n const health = await engine.health();\n return c.json(health, health.connected ? 200 : 503);\n });\n\n // List registered workflows\n app.get(\"/workflows\", (c) => {\n return c.json(engine.listWorkflows());\n });\n\n // List runs for a workflow\n app.get(\"/workflows/:id/runs\", async (c) => {\n const id = c.req.param(\"id\");\n const query = c.req.query();\n\n const result = await engine.listRuns(id, {\n limit: query.limit ? Number(query.limit) : undefined,\n cursor: query.cursor ? Number(query.cursor) : undefined,\n order: query.order as \"asc\" | \"desc\" | undefined,\n status: query.status as RunStatus | undefined,\n });\n\n return c.json(result);\n });\n\n // Get run detail\n app.get(\"/runs/:runId\", async (c) => {\n const runId = c.req.param(\"runId\");\n const run = await engine.getRun(runId);\n\n if (!run) {\n return c.json({ error: \"Run not found\" }, 404);\n }\n\n return c.json(run);\n });\n\n // Cancel a run\n app.post(\"/runs/:runId/cancel\", async (c) => {\n const runId = c.req.param(\"runId\");\n\n try {\n await engine.cancelRun(runId);\n return c.json({ success: true });\n } catch (error) {\n return c.json(\n { error: error instanceof Error ? error.message : String(error) },\n 400\n );\n }\n });\n\n // Retry a failed run\n app.post(\"/runs/:runId/retry\", async (c) => {\n const runId = c.req.param(\"runId\");\n\n try {\n await engine.retryRun(runId);\n return c.json({ success: true });\n } catch (error) {\n return c.json(\n { error: error instanceof Error ? error.message : String(error) },\n 400\n );\n }\n });\n\n // Trigger a workflow\n app.post(\"/workflows/:id/trigger\", async (c) => {\n const id = c.req.param(\"id\");\n\n try {\n const body = await c.req.json();\n const { runId } = await engine.trigger(id, body);\n return c.json({ runId }, 202);\n } catch (error) {\n return c.json(\n { error: error instanceof Error ? error.message : String(error) },\n 400\n );\n }\n });\n\n return app;\n}\n","import { Hono } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { Engine, EngineEventName } from \"chisel-engine\";\n\nconst ENGINE_EVENTS: EngineEventName[] = [\n \"workflow:start\",\n \"workflow:complete\",\n \"workflow:fail\",\n \"step:start\",\n \"step:complete\",\n \"step:fail\",\n \"step:retry\",\n];\n\nexport function createSseRoute(engine: Engine): Hono {\n const app = new Hono();\n\n app.get(\"/events\", (c) => {\n return streamSSE(c, async (stream) => {\n const handlers = new Map<string, (payload: unknown) => void>();\n\n for (const event of ENGINE_EVENTS) {\n const handler = (payload: unknown) => {\n stream\n .writeSSE({\n event,\n data: JSON.stringify(payload, (_key, value) =>\n value instanceof Error\n ? { message: value.message, name: value.name }\n : value\n ),\n })\n .catch(() => {});\n };\n handlers.set(event, handler);\n engine.on(event, handler as any);\n }\n\n // Heartbeat every 15 seconds\n const heartbeat = setInterval(() => {\n stream\n .writeSSE({\n event: \"heartbeat\",\n data: JSON.stringify({ time: Date.now() }),\n })\n .catch(() => {});\n }, 15_000);\n\n // Keep the stream alive until client disconnects\n try {\n while (true) {\n await stream.sleep(1000);\n }\n } finally {\n clearInterval(heartbeat);\n for (const [event, handler] of handlers) {\n engine.off(event as EngineEventName, handler as any);\n }\n }\n });\n });\n\n return app;\n}\n","import { readFileSync, existsSync } from \"fs\";\nimport { join, extname } from \"path\";\nimport type { Context } from \"hono\";\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"application/javascript; charset=utf-8\",\n \".mjs\": \"application/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n};\n\nexport function createStaticHandler(uiDir: string) {\n // Pre-load index.html into memory (small file, hot path)\n const indexPath = join(uiDir, \"index.html\");\n let indexHtml = \"\";\n if (existsSync(indexPath)) {\n indexHtml = readFileSync(indexPath, \"utf-8\");\n }\n\n return async (c: Context) => {\n const reqPath = new URL(c.req.url).pathname;\n\n // Try serving static asset\n const filePath = join(uiDir, reqPath);\n if (existsSync(filePath) && reqPath !== \"/\") {\n try {\n const content = readFileSync(filePath);\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || \"application/octet-stream\";\n return new Response(content, {\n headers: {\n \"Content-Type\": contentType,\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n },\n });\n } catch {\n // Fall through to index.html\n }\n }\n\n // SPA fallback: serve index.html for all non-file routes\n if (!indexHtml) {\n return c.text(\"Studio UI not built. Run: bun run build:ui\", 500);\n }\n return c.html(indexHtml);\n };\n}\n","import { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { join } from \"path\";\nimport { existsSync } from \"fs\";\nimport type { Engine } from \"chisel-engine\";\nimport { createApiRoutes } from \"./routes/api\";\nimport { createSseRoute } from \"./routes/sse\";\nimport { createStaticHandler } from \"./static\";\n\nfunction resolveUiDir(): string {\n // When running from built output: __dirname is dist/, ui is dist/ui/\n const fromDist = join(__dirname, \"ui\");\n if (existsSync(join(fromDist, \"index.html\"))) return fromDist;\n\n // When running from source via bun/tsx: __dirname is src/, ui is ../dist/ui/\n const fromSrc = join(__dirname, \"..\", \"dist\", \"ui\");\n if (existsSync(join(fromSrc, \"index.html\"))) return fromSrc;\n\n // Fallback\n return fromDist;\n}\n\nexport function createStudioApp(engine: Engine): Hono {\n const app = new Hono();\n\n // Enable CORS for development\n app.use(\"*\", cors());\n\n // API routes\n app.route(\"/api\", createApiRoutes(engine));\n\n // SSE events\n app.route(\"/api\", createSseRoute(engine));\n\n // Static SPA assets\n app.get(\"*\", createStaticHandler(resolveUiDir()));\n\n return app;\n}\n","import { serve } from \"@hono/node-server\";\nimport type { Engine } from \"chisel-engine\";\nimport { createStudioApp } from \"./server\";\nimport type { StudioOptions, StudioServer } from \"./types\";\n\nexport type { StudioOptions, StudioServer } from \"./types\";\n\nexport function createStudio(\n engine: Engine,\n options: StudioOptions = {}\n): StudioServer {\n const port = options.port ?? 4040;\n const host = options.host ?? \"localhost\";\n const url = `http://${host}:${port}`;\n\n const app = createStudioApp(engine);\n let server: ReturnType<typeof serve> | null = null;\n\n return {\n get url() {\n return url;\n },\n\n async start() {\n server = serve({ fetch: app.fetch, port, hostname: host });\n\n console.log(`Chisel Studio running at ${url}`);\n\n if (options.open) {\n const { exec } = await import(\"child_process\");\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${cmd} ${url}`);\n }\n },\n\n async stop() {\n if (server) {\n server.close();\n server = null;\n }\n },\n };\n}\n"]}
|