@boardwalk-labs/engine 0.1.4 → 0.1.6
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/dist/engine.d.ts +2 -2
- package/dist/engine.js +6 -6
- package/dist/run/supervisor.js +1 -1
- package/dist/scheduler/scheduler.js +0 -0
- package/dist/server/routes/api.d.ts +2 -2
- package/dist/server/routes/api.js +8 -8
- package/dist/server/routes/hooks.d.ts +1 -1
- package/dist/server/routes/hooks.js +17 -17
- package/dist/server/routes/ui.js +1 -1
- package/dist/server_main.js +2 -2
- package/dist/store/migrations.js +1 -1
- package/dist/store/store.d.ts +4 -4
- package/dist/store/store.js +12 -12
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export declare class Engine {
|
|
|
65
65
|
/** Subscribe to every stamped run event (feeds SSE, the CLI renderer, the log UI). */
|
|
66
66
|
onEvent(listener: (row: EventRow) => void): () => void;
|
|
67
67
|
/**
|
|
68
|
-
* Deploy (or redeploy, by manifest
|
|
68
|
+
* Deploy (or redeploy, by manifest slug) a workflow from its bundled program source. The
|
|
69
69
|
* manifest is DERIVED from the program's pure-literal `meta` — the program file is the
|
|
70
70
|
* author's source of truth, manifest drift is impossible by construction.
|
|
71
71
|
*/
|
|
@@ -74,7 +74,7 @@ export declare class Engine {
|
|
|
74
74
|
* Queue a run and dispatch it through the concurrency gate. Returns the queued row
|
|
75
75
|
* immediately; `waitForRun` for the terminal row.
|
|
76
76
|
*/
|
|
77
|
-
startRun(
|
|
77
|
+
startRun(slug: string, opts?: {
|
|
78
78
|
input?: JsonValue;
|
|
79
79
|
triggerKind?: TriggerKind;
|
|
80
80
|
}): RunRow;
|
package/dist/engine.js
CHANGED
|
@@ -75,7 +75,7 @@ export class Engine {
|
|
|
75
75
|
return this.supervisor.onEvent(listener);
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* Deploy (or redeploy, by manifest
|
|
78
|
+
* Deploy (or redeploy, by manifest slug) a workflow from its bundled program source. The
|
|
79
79
|
* manifest is DERIVED from the program's pure-literal `meta` — the program file is the
|
|
80
80
|
* author's source of truth, manifest drift is impossible by construction.
|
|
81
81
|
*/
|
|
@@ -83,7 +83,7 @@ export class Engine {
|
|
|
83
83
|
this.assertOpen();
|
|
84
84
|
const manifest = extractManifest(args.program, { fileName: "index.mjs" });
|
|
85
85
|
const workflow = this.store.upsertWorkflow({
|
|
86
|
-
|
|
86
|
+
slug: manifest.slug,
|
|
87
87
|
manifest,
|
|
88
88
|
program: args.program,
|
|
89
89
|
...(args.config !== undefined ? { config: args.config } : {}),
|
|
@@ -106,11 +106,11 @@ export class Engine {
|
|
|
106
106
|
* Queue a run and dispatch it through the concurrency gate. Returns the queued row
|
|
107
107
|
* immediately; `waitForRun` for the terminal row.
|
|
108
108
|
*/
|
|
109
|
-
startRun(
|
|
109
|
+
startRun(slug, opts = {}) {
|
|
110
110
|
this.assertOpen();
|
|
111
|
-
const workflow = this.store.getWorkflow(
|
|
111
|
+
const workflow = this.store.getWorkflow(slug);
|
|
112
112
|
if (workflow === null) {
|
|
113
|
-
throw new EngineError("NOT_FOUND", `Workflow "${
|
|
113
|
+
throw new EngineError("NOT_FOUND", `Workflow "${slug}" is not deployed on this engine.`);
|
|
114
114
|
}
|
|
115
115
|
const { run } = this.store.createRun({
|
|
116
116
|
workflowId: workflow.id,
|
|
@@ -168,7 +168,7 @@ export class Engine {
|
|
|
168
168
|
*/
|
|
169
169
|
async runOnce(args) {
|
|
170
170
|
const workflow = this.deployWorkflow(args);
|
|
171
|
-
const run = this.startRun(workflow.
|
|
171
|
+
const run = this.startRun(workflow.slug, {
|
|
172
172
|
...(args.input !== undefined ? { input: args.input } : {}),
|
|
173
173
|
});
|
|
174
174
|
return await this.waitForRun(run.id);
|
package/dist/run/supervisor.js
CHANGED
|
@@ -510,7 +510,7 @@ export class RunSupervisor {
|
|
|
510
510
|
startChildRun(parentRunId, slug, input, idempotencyKey) {
|
|
511
511
|
const target = this.store.getWorkflow(slug);
|
|
512
512
|
if (target === null) {
|
|
513
|
-
throw new EngineError("NOT_FOUND", `workflows.call target "${slug}" is not deployed on this engine.`, `Deploy it first — the engine only runs workflows it knows by
|
|
513
|
+
throw new EngineError("NOT_FOUND", `workflows.call target "${slug}" is not deployed on this engine.`, `Deploy it first — the engine only runs workflows it knows by slug.`);
|
|
514
514
|
}
|
|
515
515
|
// Crossed the JSON IPC channel, but narrow instead of assuming — and
|
|
516
516
|
// the canonical default key requires a JSON tree anyway.
|
|
Binary file
|
|
@@ -3,8 +3,8 @@ import type { RouteContext } from "./router.js";
|
|
|
3
3
|
export declare function handleListWorkflows(ctx: RouteContext): void;
|
|
4
4
|
/** GET /api/runs?workflow=&status=&limit=&offset= — newest first, full RunRow shape. */
|
|
5
5
|
export declare function handleListRuns(ctx: RouteContext): void;
|
|
6
|
-
/** POST /api/workflows/:
|
|
7
|
-
export declare function handleStartRun(ctx: RouteContext,
|
|
6
|
+
/** POST /api/workflows/:slug/runs — start a manual run; 201 with the queued row. */
|
|
7
|
+
export declare function handleStartRun(ctx: RouteContext, slug: string): Promise<void>;
|
|
8
8
|
/** GET /api/runs/:id */
|
|
9
9
|
export declare function handleGetRun(ctx: RouteContext, runId: string): void;
|
|
10
10
|
/**
|
|
@@ -29,7 +29,7 @@ function isRunStatus(value) {
|
|
|
29
29
|
/** GET /api/workflows — names + manifest-derived fields, enough to render a picker. */
|
|
30
30
|
export function handleListWorkflows(ctx) {
|
|
31
31
|
const workflows = ctx.engine.store.listWorkflows().map((row) => ({
|
|
32
|
-
|
|
32
|
+
slug: row.slug,
|
|
33
33
|
description: row.manifest.description ?? null,
|
|
34
34
|
triggers: row.manifest.triggers,
|
|
35
35
|
createdAt: row.createdAt,
|
|
@@ -43,11 +43,11 @@ export function handleListRuns(ctx) {
|
|
|
43
43
|
limit: parseNonNegativeInt(ctx.url, "limit", DEFAULT_RUNS_LIMIT),
|
|
44
44
|
offset: parseNonNegativeInt(ctx.url, "offset", 0),
|
|
45
45
|
};
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
const workflow = ctx.engine.store.getWorkflow(
|
|
46
|
+
const slug = ctx.url.searchParams.get("workflow");
|
|
47
|
+
if (slug !== null) {
|
|
48
|
+
const workflow = ctx.engine.store.getWorkflow(slug);
|
|
49
49
|
if (workflow === null) {
|
|
50
|
-
throw new HttpError(404, "NOT_FOUND", `Workflow "${
|
|
50
|
+
throw new HttpError(404, "NOT_FOUND", `Workflow "${slug}" is not deployed on this engine.`);
|
|
51
51
|
}
|
|
52
52
|
filter.workflowId = workflow.id;
|
|
53
53
|
}
|
|
@@ -60,12 +60,12 @@ export function handleListRuns(ctx) {
|
|
|
60
60
|
}
|
|
61
61
|
sendJson(ctx.res, 200, { runs: ctx.engine.store.listRuns(filter) });
|
|
62
62
|
}
|
|
63
|
-
/** POST /api/workflows/:
|
|
64
|
-
export async function handleStartRun(ctx,
|
|
63
|
+
/** POST /api/workflows/:slug/runs — start a manual run; 201 with the queued row. */
|
|
64
|
+
export async function handleStartRun(ctx, slug) {
|
|
65
65
|
const raw = await readBody(ctx.req, MAX_BODY_BYTES);
|
|
66
66
|
// An empty body means "no input" — the curl-without-data ergonomics of a run-now button.
|
|
67
67
|
const body = raw.length === 0 ? {} : parseJsonBody(raw, startRunBodySchema, "run-start body");
|
|
68
|
-
const run = ctx.engine.startRun(
|
|
68
|
+
const run = ctx.engine.startRun(slug, {
|
|
69
69
|
triggerKind: "manual",
|
|
70
70
|
...(body.input !== undefined ? { input: body.input } : {}),
|
|
71
71
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RouteContext } from "./router.js";
|
|
2
|
-
export declare function handleWebhook(ctx: RouteContext,
|
|
2
|
+
export declare function handleWebhook(ctx: RouteContext, slug: string, triggerIndexRaw: string): Promise<void>;
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
// token auth: BOARDWALK_WEBHOOK_TOKEN__<NAME> vs `Authorization: Bearer <token>`
|
|
6
6
|
// signature auth: BOARDWALK_WEBHOOK_SECRET__<NAME> vs `X-Boardwalk-Signature: sha256=<hex>`
|
|
7
7
|
// (HMAC-SHA256 over the raw request body)
|
|
8
|
-
// where <NAME> is the workflow
|
|
8
|
+
// where <NAME> is the workflow slug upper-cased with `-` → `_`. Missing variable = 503 (fail
|
|
9
9
|
// closed, hint names the variable); bad credential = 401. These are server config, not
|
|
10
10
|
// workflow secrets, so they resolve from process.env — never from the engine's env map.
|
|
11
11
|
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
12
12
|
import { HttpError, MAX_BODY_BYTES, jsonValueSchema, parseJsonBody, readBody, sendJson, } from "../http.js";
|
|
13
|
-
export async function handleWebhook(ctx,
|
|
13
|
+
export async function handleWebhook(ctx, slug, triggerIndexRaw) {
|
|
14
14
|
// The raw body is read up front: signature auth signs the exact bytes on the wire, before
|
|
15
15
|
// any JSON parsing can normalize them away.
|
|
16
16
|
const rawBody = await readBody(ctx.req, MAX_BODY_BYTES);
|
|
17
17
|
// One identical 404 for "no workflow", "no such trigger index", and "not a webhook
|
|
18
18
|
// trigger" — an unauthenticated caller learns nothing about what is deployed here.
|
|
19
|
-
const notFound = new HttpError(404, "NOT_FOUND", `No webhook trigger at /hooks/${
|
|
20
|
-
const workflow = ctx.engine.store.getWorkflow(
|
|
19
|
+
const notFound = new HttpError(404, "NOT_FOUND", `No webhook trigger at /hooks/${slug}/${triggerIndexRaw}.`);
|
|
20
|
+
const workflow = ctx.engine.store.getWorkflow(slug);
|
|
21
21
|
if (workflow === null)
|
|
22
22
|
throw notFound;
|
|
23
23
|
if (!/^\d+$/.test(triggerIndexRaw))
|
|
@@ -26,27 +26,27 @@ export async function handleWebhook(ctx, workflowName, triggerIndexRaw) {
|
|
|
26
26
|
if (trigger === undefined || trigger.kind !== "webhook")
|
|
27
27
|
throw notFound;
|
|
28
28
|
if (trigger.auth === "token")
|
|
29
|
-
authorizeToken(ctx.req,
|
|
29
|
+
authorizeToken(ctx.req, slug);
|
|
30
30
|
else
|
|
31
|
-
authorizeSignature(ctx.req,
|
|
31
|
+
authorizeSignature(ctx.req, slug, rawBody);
|
|
32
32
|
const input = rawBody.length === 0 ? null : parseJsonBody(rawBody, jsonValueSchema, "webhook payload");
|
|
33
|
-
const run = ctx.engine.startRun(
|
|
33
|
+
const run = ctx.engine.startRun(slug, { input, triggerKind: "webhook" });
|
|
34
34
|
sendJson(ctx.res, 201, { run: { id: run.id, status: run.status } });
|
|
35
35
|
}
|
|
36
|
-
/** `BOARDWALK_WEBHOOK_<kind>__<NAME>`: the workflow
|
|
37
|
-
function webhookEnvVarName(kind,
|
|
38
|
-
return `BOARDWALK_WEBHOOK_${kind}__${
|
|
36
|
+
/** `BOARDWALK_WEBHOOK_<kind>__<NAME>`: the workflow slug upper-cased, hyphens → underscores. */
|
|
37
|
+
function webhookEnvVarName(kind, slug) {
|
|
38
|
+
return `BOARDWALK_WEBHOOK_${kind}__${slug.toUpperCase().replaceAll("-", "_")}`;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Read the trigger's credential from the server environment, failing CLOSED when unset: a
|
|
42
42
|
* webhook that nobody configured must never become an open trigger. Read lazily per request
|
|
43
43
|
* so an operator can fix the environment without redeploying workflows.
|
|
44
44
|
*/
|
|
45
|
-
function requiredCredential(kind,
|
|
46
|
-
const name = webhookEnvVarName(kind,
|
|
45
|
+
function requiredCredential(kind, slug) {
|
|
46
|
+
const name = webhookEnvVarName(kind, slug);
|
|
47
47
|
const value = process.env[name];
|
|
48
48
|
if (value === undefined || value === "") {
|
|
49
|
-
throw new HttpError(503, "WEBHOOK_UNCONFIGURED", `Webhook auth for workflow "${
|
|
49
|
+
throw new HttpError(503, "WEBHOOK_UNCONFIGURED", `Webhook auth for workflow "${slug}" is not configured on this server.`, `Set the environment variable ${name} and restart the server.`);
|
|
50
50
|
}
|
|
51
51
|
return value;
|
|
52
52
|
}
|
|
@@ -54,16 +54,16 @@ function requiredCredential(kind, workflowName) {
|
|
|
54
54
|
function unauthorized() {
|
|
55
55
|
return new HttpError(401, "UNAUTHORIZED", "Invalid webhook credentials.");
|
|
56
56
|
}
|
|
57
|
-
function authorizeToken(req,
|
|
58
|
-
const expected = requiredCredential("TOKEN",
|
|
57
|
+
function authorizeToken(req, slug) {
|
|
58
|
+
const expected = requiredCredential("TOKEN", slug);
|
|
59
59
|
const header = req.headers.authorization;
|
|
60
60
|
if (header === undefined || !header.startsWith("Bearer "))
|
|
61
61
|
throw unauthorized();
|
|
62
62
|
if (!constantTimeEquals(header.slice("Bearer ".length), expected))
|
|
63
63
|
throw unauthorized();
|
|
64
64
|
}
|
|
65
|
-
function authorizeSignature(req,
|
|
66
|
-
const secret = requiredCredential("SECRET",
|
|
65
|
+
function authorizeSignature(req, slug, rawBody) {
|
|
66
|
+
const secret = requiredCredential("SECRET", slug);
|
|
67
67
|
const header = req.headers["x-boardwalk-signature"];
|
|
68
68
|
if (typeof header !== "string")
|
|
69
69
|
throw unauthorized();
|
package/dist/server/routes/ui.js
CHANGED
|
@@ -70,7 +70,7 @@ async function loadWorkflows() {
|
|
|
70
70
|
}
|
|
71
71
|
box.append(rowButton("all workflows", () => { selectedWorkflow = null; loadRuns(); }));
|
|
72
72
|
for (const workflow of workflows) {
|
|
73
|
-
box.append(rowButton(workflow.
|
|
73
|
+
box.append(rowButton(workflow.slug, () => { selectedWorkflow = workflow.slug; loadRuns(); }));
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
package/dist/server_main.js
CHANGED
|
@@ -122,7 +122,7 @@ export function loadServerConfig(env) {
|
|
|
122
122
|
/**
|
|
123
123
|
* Deploy every built workflow in `dir` on boot — the self-host deploy mechanism. Each `.mjs`/`.js`
|
|
124
124
|
* file is one workflow's program (single-file, `@boardwalk-labs/workflow` external — what
|
|
125
|
-
* `boardwalk build` emits). Idempotent by manifest
|
|
125
|
+
* `boardwalk build` emits). Idempotent by manifest slug (re-boot re-syncs the dir into the store);
|
|
126
126
|
* a removed file leaves its last-deployed workflow in place (no un-deploy in v0). A missing dir is
|
|
127
127
|
* fine (an operator may deploy by other means); a bad file is logged and skipped, never fatal.
|
|
128
128
|
*/
|
|
@@ -143,7 +143,7 @@ function deployWorkflowsFromDir(engine, dir, log) {
|
|
|
143
143
|
try {
|
|
144
144
|
const program = readFileSync(join(dir, file), "utf8");
|
|
145
145
|
const workflow = engine.deployWorkflow({ program });
|
|
146
|
-
log(`deployed "${workflow.
|
|
146
|
+
log(`deployed "${workflow.slug}" from ${file}`);
|
|
147
147
|
}
|
|
148
148
|
catch (err) {
|
|
149
149
|
log(`skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
package/dist/store/migrations.js
CHANGED
package/dist/store/store.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { RunStatus };
|
|
|
3
3
|
/** A deployed workflow: validated manifest + bundled program source + per-deploy config. */
|
|
4
4
|
export interface WorkflowRow {
|
|
5
5
|
id: string;
|
|
6
|
-
|
|
6
|
+
slug: string;
|
|
7
7
|
manifest: WorkflowManifest;
|
|
8
8
|
program: string;
|
|
9
9
|
config: Record<string, JsonValue>;
|
|
@@ -80,17 +80,17 @@ export declare class Store {
|
|
|
80
80
|
*/
|
|
81
81
|
transaction<T>(fn: () => T): T;
|
|
82
82
|
/**
|
|
83
|
-
* Insert a workflow or update it by
|
|
83
|
+
* Insert a workflow or update it by slug (deploying again is always an update — the slug is
|
|
84
84
|
* the user-facing identity, so the id stays stable across redeploys and existing runs keep
|
|
85
85
|
* their foreign keys). `updated_at` bumps on update; `created_at` and `id` never change.
|
|
86
86
|
*/
|
|
87
87
|
upsertWorkflow(args: {
|
|
88
|
-
|
|
88
|
+
slug: string;
|
|
89
89
|
manifest: WorkflowManifest;
|
|
90
90
|
program: string;
|
|
91
91
|
config?: Record<string, JsonValue>;
|
|
92
92
|
}): WorkflowRow;
|
|
93
|
-
getWorkflow(
|
|
93
|
+
getWorkflow(slug: string): WorkflowRow | null;
|
|
94
94
|
getWorkflowById(id: string): WorkflowRow | null;
|
|
95
95
|
listWorkflows(): WorkflowRow[];
|
|
96
96
|
/**
|
package/dist/store/store.js
CHANGED
|
@@ -113,7 +113,7 @@ function readEnum(row, table, column, schema) {
|
|
|
113
113
|
function mapWorkflow(row) {
|
|
114
114
|
return {
|
|
115
115
|
id: readText(row, "workflows", "id"),
|
|
116
|
-
|
|
116
|
+
slug: readText(row, "workflows", "slug"),
|
|
117
117
|
manifest: readJson(row, "workflows", "manifest", workflowManifestSchema),
|
|
118
118
|
program: readText(row, "workflows", "program"),
|
|
119
119
|
config: readJson(row, "workflows", "config", configSchema),
|
|
@@ -267,7 +267,7 @@ export class Store {
|
|
|
267
267
|
// Workflows
|
|
268
268
|
// --------------------------------------------------------------------------
|
|
269
269
|
/**
|
|
270
|
-
* Insert a workflow or update it by
|
|
270
|
+
* Insert a workflow or update it by slug (deploying again is always an update — the slug is
|
|
271
271
|
* the user-facing identity, so the id stays stable across redeploys and existing runs keep
|
|
272
272
|
* their foreign keys). `updated_at` bumps on update; `created_at` and `id` never change.
|
|
273
273
|
*/
|
|
@@ -276,29 +276,29 @@ export class Store {
|
|
|
276
276
|
// caller bug is better rejected here than persisted and discovered as INTERNAL on read.
|
|
277
277
|
const manifest = workflowManifestSchema.safeParse(args.manifest);
|
|
278
278
|
if (!manifest.success) {
|
|
279
|
-
throw new EngineError("VALIDATION", `manifest for workflow "${args.
|
|
279
|
+
throw new EngineError("VALIDATION", `manifest for workflow "${args.slug}" failed validation: ${manifest.error.message}`);
|
|
280
280
|
}
|
|
281
281
|
const manifestJson = JSON.stringify(manifest.data);
|
|
282
282
|
const configJson = JSON.stringify(args.config ?? {});
|
|
283
283
|
return this.transaction(() => {
|
|
284
284
|
const t = this.now();
|
|
285
|
-
const existing = this.prepare("SELECT id FROM workflows WHERE
|
|
285
|
+
const existing = this.prepare("SELECT id FROM workflows WHERE slug = ?").get(args.slug);
|
|
286
286
|
if (existing === undefined) {
|
|
287
|
-
this.prepare(`INSERT INTO workflows (id,
|
|
288
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(ulid(t), args.
|
|
287
|
+
this.prepare(`INSERT INTO workflows (id, slug, manifest, program, config, created_at, updated_at)
|
|
288
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(ulid(t), args.slug, manifestJson, args.program, configJson, t, t);
|
|
289
289
|
}
|
|
290
290
|
else {
|
|
291
|
-
this.prepare("UPDATE workflows SET manifest = ?, program = ?, config = ?, updated_at = ? WHERE
|
|
291
|
+
this.prepare("UPDATE workflows SET manifest = ?, program = ?, config = ?, updated_at = ? WHERE slug = ?").run(manifestJson, args.program, configJson, t, args.slug);
|
|
292
292
|
}
|
|
293
|
-
const row = this.getWorkflow(args.
|
|
293
|
+
const row = this.getWorkflow(args.slug);
|
|
294
294
|
if (row === null) {
|
|
295
|
-
throw new EngineError("INTERNAL", `workflow "${args.
|
|
295
|
+
throw new EngineError("INTERNAL", `workflow "${args.slug}" vanished mid-upsert`);
|
|
296
296
|
}
|
|
297
297
|
return row;
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
|
-
getWorkflow(
|
|
301
|
-
const row = this.prepare("SELECT * FROM workflows WHERE
|
|
300
|
+
getWorkflow(slug) {
|
|
301
|
+
const row = this.prepare("SELECT * FROM workflows WHERE slug = ?").get(slug);
|
|
302
302
|
return row === undefined ? null : mapWorkflow(row);
|
|
303
303
|
}
|
|
304
304
|
getWorkflowById(id) {
|
|
@@ -306,7 +306,7 @@ export class Store {
|
|
|
306
306
|
return row === undefined ? null : mapWorkflow(row);
|
|
307
307
|
}
|
|
308
308
|
listWorkflows() {
|
|
309
|
-
return this.prepare("SELECT * FROM workflows ORDER BY
|
|
309
|
+
return this.prepare("SELECT * FROM workflows ORDER BY slug ASC").all().map(mapWorkflow);
|
|
310
310
|
}
|
|
311
311
|
// --------------------------------------------------------------------------
|
|
312
312
|
// Runs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boardwalk-labs/engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The Boardwalk single-node engine: cron scheduling, run lifecycle, SQLite state, and the local run log. Powers `boardwalk dev` and the self-hosted server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|