workplane 0.3.1 → 0.4.1
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 +80 -86
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/migrate.js +260 -41
- package/dist/migrate.js.map +1 -1
- package/dist/migrations/pg/0000_majestic_norman_osborn.sql +75 -0
- package/dist/migrations/pg/meta/0000_snapshot.json +583 -0
- package/dist/migrations/pg/meta/_journal.json +13 -0
- package/dist/migrations/sqlite/0000_dry_zarek.sql +75 -0
- package/dist/migrations/sqlite/meta/0000_snapshot.json +535 -0
- package/dist/migrations/sqlite/meta/_journal.json +13 -0
- package/dist/node.js +20 -17
- package/dist/node.js.map +1 -1
- package/dist/schema.sqlite.sql +74 -0
- package/dist/server.js +743 -335
- package/dist/server.js.map +1 -1
- package/dist/setup.js +410 -0
- package/dist/setup.js.map +1 -0
- package/package.json +8 -2
package/dist/migrate.js
CHANGED
|
@@ -1,29 +1,125 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
2
11
|
|
|
3
|
-
// ../db/src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
12
|
+
// ../db/src/schema/sqlite.ts
|
|
13
|
+
var sqlite_exports = {};
|
|
14
|
+
__export(sqlite_exports, {
|
|
15
|
+
artifacts: () => artifacts2,
|
|
16
|
+
nodes: () => nodes2,
|
|
17
|
+
runInputEvents: () => runInputEvents2,
|
|
18
|
+
runLogs: () => runLogs2,
|
|
19
|
+
runs: () => runs2,
|
|
20
|
+
tasks: () => tasks2
|
|
21
|
+
});
|
|
22
|
+
import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
|
|
23
|
+
import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
|
|
24
|
+
var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, runInputEvents2;
|
|
25
|
+
var init_sqlite = __esm({
|
|
26
|
+
"../db/src/schema/sqlite.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
nowDefault = sql3`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`;
|
|
29
|
+
tasks2 = sqliteTable("tasks", {
|
|
30
|
+
id: text2("id").primaryKey(),
|
|
31
|
+
kind: text2("kind").notNull(),
|
|
32
|
+
adapter: text2("adapter").notNull(),
|
|
33
|
+
payload: text2("payload", { mode: "json" }).$type().notNull().default({}),
|
|
34
|
+
requires: text2("requires", { mode: "json" }).$type().notNull().default([]),
|
|
35
|
+
status: text2("status").notNull(),
|
|
36
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
37
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
38
|
+
}, (table) => ({
|
|
39
|
+
statusCreatedAtIdx: index2("idx_tasks_status_created_at").on(table.status, table.createdAt)
|
|
40
|
+
}));
|
|
41
|
+
nodes2 = sqliteTable("nodes", {
|
|
42
|
+
id: text2("id").primaryKey(),
|
|
43
|
+
name: text2("name").notNull(),
|
|
44
|
+
capabilities: text2("capabilities", { mode: "json" }).$type().notNull().default([]),
|
|
45
|
+
status: text2("status").notNull().default("online"),
|
|
46
|
+
lastHeartbeatAt: text2("last_heartbeat_at"),
|
|
47
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
48
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
49
|
+
});
|
|
50
|
+
runs2 = sqliteTable("runs", {
|
|
51
|
+
id: text2("id").primaryKey(),
|
|
52
|
+
taskId: text2("task_id").notNull().references(() => tasks2.id),
|
|
53
|
+
nodeId: text2("node_id").notNull().references(() => nodes2.id),
|
|
54
|
+
attempt: integer2("attempt").notNull(),
|
|
55
|
+
status: text2("status").notNull(),
|
|
56
|
+
startedAt: text2("started_at"),
|
|
57
|
+
endedAt: text2("ended_at"),
|
|
58
|
+
error: text2("error"),
|
|
59
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
60
|
+
updatedAt: text2("updated_at").notNull().default(nowDefault)
|
|
61
|
+
}, (table) => ({
|
|
62
|
+
taskIdIdx: index2("idx_runs_task_id").on(table.taskId),
|
|
63
|
+
nodeIdIdx: index2("idx_runs_node_id").on(table.nodeId)
|
|
64
|
+
}));
|
|
65
|
+
runLogs2 = sqliteTable("run_logs", {
|
|
66
|
+
id: integer2("id").primaryKey({ autoIncrement: true }),
|
|
67
|
+
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
68
|
+
stepName: text2("step_name"),
|
|
69
|
+
stream: text2("stream").notNull(),
|
|
70
|
+
message: text2("message").notNull(),
|
|
71
|
+
timestamp: text2("timestamp").notNull().default(nowDefault)
|
|
72
|
+
}, (table) => ({
|
|
73
|
+
runIdTimestampIdx: index2("idx_run_logs_run_id_timestamp").on(table.runId, table.timestamp)
|
|
74
|
+
}));
|
|
75
|
+
artifacts2 = sqliteTable("artifacts", {
|
|
76
|
+
id: text2("id").primaryKey(),
|
|
77
|
+
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
78
|
+
type: text2("type").notNull(),
|
|
79
|
+
name: text2("name").notNull(),
|
|
80
|
+
path: text2("path").notNull(),
|
|
81
|
+
metadata: text2("metadata", { mode: "json" }).$type(),
|
|
82
|
+
createdAt: text2("created_at").notNull().default(nowDefault)
|
|
83
|
+
}, (table) => ({
|
|
84
|
+
runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
|
|
85
|
+
}));
|
|
86
|
+
runInputEvents2 = sqliteTable("run_input_events", {
|
|
87
|
+
id: integer2("id").primaryKey({ autoIncrement: true }),
|
|
88
|
+
runId: text2("run_id").notNull().references(() => runs2.id),
|
|
89
|
+
sequence: integer2("sequence").notNull(),
|
|
90
|
+
kind: text2("kind").notNull(),
|
|
91
|
+
payload: text2("payload", { mode: "json" }).$type().notNull().default({}),
|
|
92
|
+
createdAt: text2("created_at").notNull().default(nowDefault),
|
|
93
|
+
deliveredAt: text2("delivered_at")
|
|
94
|
+
}, (table) => ({
|
|
95
|
+
runSequenceUniqueIdx: uniqueIndex2("idx_run_input_events_run_sequence").on(table.runId, table.sequence),
|
|
96
|
+
undeliveredIdx: index2("idx_run_input_events_undelivered").on(table.runId, table.sequence).where(isNull2(table.deliveredAt))
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
7
100
|
|
|
8
101
|
// ../core/src/config.ts
|
|
9
102
|
import { z } from "zod";
|
|
10
103
|
|
|
11
104
|
// ../core/src/env.ts
|
|
12
105
|
import { config as dotenvConfig } from "dotenv";
|
|
106
|
+
import { homedir } from "os";
|
|
107
|
+
import { join } from "path";
|
|
13
108
|
var envLoaded = false;
|
|
14
109
|
function loadLocalEnv() {
|
|
15
110
|
if (envLoaded) {
|
|
16
111
|
return;
|
|
17
112
|
}
|
|
113
|
+
dotenvConfig({ path: ".env.local" });
|
|
114
|
+
dotenvConfig({ path: join(homedir(), ".workplane", ".env") });
|
|
18
115
|
dotenvConfig({ path: ".env" });
|
|
19
|
-
dotenvConfig({ path: ".env.local", override: true });
|
|
20
116
|
envLoaded = true;
|
|
21
117
|
}
|
|
22
118
|
|
|
23
119
|
// ../core/src/config.ts
|
|
24
120
|
loadLocalEnv();
|
|
25
121
|
var serverEnvSchema = z.object({
|
|
26
|
-
DATABASE_URL: z.string().
|
|
122
|
+
DATABASE_URL: z.string().optional().default("sqlite://~/.workplane/workplane.db"),
|
|
27
123
|
WORKPLANE_SERVER_PORT: z.string().optional().default("8787").transform((value) => Number.parseInt(value, 10)),
|
|
28
124
|
WORKPLANE_NODE_TOKEN: z.string().optional(),
|
|
29
125
|
WORKPLANE_OPERATOR_TOKEN: z.string().optional(),
|
|
@@ -52,65 +148,188 @@ function parseCsv(value) {
|
|
|
52
148
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
53
149
|
}
|
|
54
150
|
|
|
151
|
+
// ../db/src/migration.ts
|
|
152
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
153
|
+
import { homedir as homedir3 } from "os";
|
|
154
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
155
|
+
import { fileURLToPath } from "url";
|
|
156
|
+
|
|
55
157
|
// ../db/src/client.ts
|
|
158
|
+
import { mkdirSync } from "fs";
|
|
159
|
+
import { homedir as homedir2 } from "os";
|
|
160
|
+
import { dirname, join as join2 } from "path";
|
|
161
|
+
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
|
162
|
+
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
|
163
|
+
import Database from "better-sqlite3";
|
|
56
164
|
import { Pool } from "pg";
|
|
165
|
+
|
|
166
|
+
// ../db/src/pg-store.ts
|
|
167
|
+
import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
|
|
168
|
+
|
|
169
|
+
// ../db/src/schema/pg.ts
|
|
170
|
+
var pg_exports = {};
|
|
171
|
+
__export(pg_exports, {
|
|
172
|
+
artifacts: () => artifacts,
|
|
173
|
+
nodes: () => nodes,
|
|
174
|
+
runInputEvents: () => runInputEvents,
|
|
175
|
+
runLogs: () => runLogs,
|
|
176
|
+
runs: () => runs,
|
|
177
|
+
tasks: () => tasks
|
|
178
|
+
});
|
|
179
|
+
import { isNull, sql } from "drizzle-orm";
|
|
180
|
+
import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
181
|
+
var tasks = pgTable("tasks", {
|
|
182
|
+
id: text("id").primaryKey(),
|
|
183
|
+
kind: text("kind").notNull(),
|
|
184
|
+
adapter: text("adapter").notNull(),
|
|
185
|
+
payload: jsonb("payload").$type().notNull(),
|
|
186
|
+
requires: text("requires").array().notNull().default(sql`'{}'::text[]`),
|
|
187
|
+
status: text("status").notNull(),
|
|
188
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
189
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
190
|
+
}, (table) => ({
|
|
191
|
+
statusCreatedAtIdx: index("idx_tasks_status_created_at").on(table.status, table.createdAt)
|
|
192
|
+
}));
|
|
193
|
+
var nodes = pgTable("nodes", {
|
|
194
|
+
id: text("id").primaryKey(),
|
|
195
|
+
name: text("name").notNull(),
|
|
196
|
+
capabilities: text("capabilities").array().notNull().default(sql`'{}'::text[]`),
|
|
197
|
+
status: text("status").notNull().default("online"),
|
|
198
|
+
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
|
|
199
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
200
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
201
|
+
});
|
|
202
|
+
var runs = pgTable("runs", {
|
|
203
|
+
id: text("id").primaryKey(),
|
|
204
|
+
taskId: text("task_id").notNull().references(() => tasks.id),
|
|
205
|
+
nodeId: text("node_id").notNull().references(() => nodes.id),
|
|
206
|
+
attempt: integer("attempt").notNull(),
|
|
207
|
+
status: text("status").notNull(),
|
|
208
|
+
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
209
|
+
endedAt: timestamp("ended_at", { withTimezone: true }),
|
|
210
|
+
error: text("error"),
|
|
211
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
212
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
213
|
+
}, (table) => ({
|
|
214
|
+
taskIdIdx: index("idx_runs_task_id").on(table.taskId),
|
|
215
|
+
nodeIdIdx: index("idx_runs_node_id").on(table.nodeId)
|
|
216
|
+
}));
|
|
217
|
+
var runLogs = pgTable("run_logs", {
|
|
218
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
219
|
+
runId: text("run_id").notNull().references(() => runs.id),
|
|
220
|
+
stepName: text("step_name"),
|
|
221
|
+
stream: text("stream").notNull(),
|
|
222
|
+
message: text("message").notNull(),
|
|
223
|
+
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
|
|
224
|
+
}, (table) => ({
|
|
225
|
+
runIdTimestampIdx: index("idx_run_logs_run_id_timestamp").on(table.runId, table.timestamp)
|
|
226
|
+
}));
|
|
227
|
+
var artifacts = pgTable("artifacts", {
|
|
228
|
+
id: text("id").primaryKey(),
|
|
229
|
+
runId: text("run_id").notNull().references(() => runs.id),
|
|
230
|
+
type: text("type").notNull(),
|
|
231
|
+
name: text("name").notNull(),
|
|
232
|
+
path: text("path").notNull(),
|
|
233
|
+
metadata: jsonb("metadata").$type(),
|
|
234
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
235
|
+
}, (table) => ({
|
|
236
|
+
runIdIdx: index("idx_artifacts_run_id").on(table.runId)
|
|
237
|
+
}));
|
|
238
|
+
var runInputEvents = pgTable("run_input_events", {
|
|
239
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
240
|
+
runId: text("run_id").notNull().references(() => runs.id),
|
|
241
|
+
sequence: bigint("sequence", { mode: "number" }).notNull(),
|
|
242
|
+
kind: text("kind").notNull(),
|
|
243
|
+
payload: jsonb("payload").$type().notNull(),
|
|
244
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
245
|
+
deliveredAt: timestamp("delivered_at", { withTimezone: true })
|
|
246
|
+
}, (table) => ({
|
|
247
|
+
runSequenceUniqueIdx: uniqueIndex("idx_run_input_events_run_sequence").on(table.runId, table.sequence),
|
|
248
|
+
undeliveredIdx: index("idx_run_input_events_undelivered").on(table.runId, table.sequence).where(isNull(table.deliveredAt))
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
// ../db/src/sqlite-store.ts
|
|
252
|
+
import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
|
|
253
|
+
init_sqlite();
|
|
254
|
+
|
|
255
|
+
// ../db/src/client.ts
|
|
256
|
+
init_sqlite();
|
|
57
257
|
var pools = /* @__PURE__ */ new Map();
|
|
58
258
|
function getPool(databaseUrl) {
|
|
59
259
|
const existing = pools.get(databaseUrl);
|
|
60
|
-
if (existing)
|
|
61
|
-
return existing;
|
|
62
|
-
}
|
|
260
|
+
if (existing) return existing;
|
|
63
261
|
const pool = new Pool({ connectionString: databaseUrl });
|
|
64
262
|
pools.set(databaseUrl, pool);
|
|
65
263
|
return pool;
|
|
66
264
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
function quoteIdentifier(value) {
|
|
70
|
-
return `"${value.replaceAll('"', '""')}"`;
|
|
71
|
-
}
|
|
72
|
-
function parseDatabaseName(databaseUrl) {
|
|
73
|
-
const url = new URL(databaseUrl);
|
|
74
|
-
const name = url.pathname.replace(/^\//, "");
|
|
75
|
-
if (!name) {
|
|
76
|
-
throw new Error("DATABASE_URL must include a database name");
|
|
77
|
-
}
|
|
78
|
-
return name;
|
|
265
|
+
function getDrizzle(pool) {
|
|
266
|
+
return drizzlePg(pool, { schema: pg_exports });
|
|
79
267
|
}
|
|
80
|
-
|
|
81
|
-
|
|
268
|
+
|
|
269
|
+
// ../db/src/migration.ts
|
|
270
|
+
var distDir = dirname2(fileURLToPath(import.meta.url));
|
|
271
|
+
async function migratePostgres(databaseUrl) {
|
|
82
272
|
const adminUrl = new URL(databaseUrl);
|
|
273
|
+
const dbName = adminUrl.pathname.replace(/^\//, "");
|
|
274
|
+
if (!dbName) throw new Error("DATABASE_URL must include a database name");
|
|
83
275
|
adminUrl.pathname = "/postgres";
|
|
84
276
|
const adminPool = getPool(adminUrl.toString());
|
|
85
277
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
278
|
+
const { rowCount } = await adminPool.query(
|
|
279
|
+
"SELECT 1 FROM pg_database WHERE datname = $1",
|
|
280
|
+
[dbName]
|
|
281
|
+
);
|
|
282
|
+
if (!rowCount) {
|
|
283
|
+
await adminPool.query(`CREATE DATABASE "${dbName.replaceAll('"', '""')}"`);
|
|
284
|
+
process.stdout.write(`Created database: ${dbName}
|
|
92
285
|
`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const e = err;
|
|
289
|
+
if (e.code === "42501") {
|
|
96
290
|
throw new Error(
|
|
97
|
-
`Database "${
|
|
291
|
+
`Database "${dbName}" does not exist and the current role lacks CREATE DATABASE permissions.`
|
|
98
292
|
);
|
|
99
293
|
}
|
|
100
|
-
throw
|
|
294
|
+
throw err;
|
|
101
295
|
} finally {
|
|
102
296
|
await adminPool.end();
|
|
103
297
|
}
|
|
298
|
+
const pool = getPool(databaseUrl);
|
|
299
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
300
|
+
await migrate(getDrizzle(pool), { migrationsFolder: resolve(distDir, "migrations/pg") });
|
|
301
|
+
await pool.end();
|
|
302
|
+
process.stdout.write("Postgres migrations applied.\n");
|
|
104
303
|
}
|
|
304
|
+
async function migrateSqlite(databaseUrl) {
|
|
305
|
+
const { default: Database2 } = await import("better-sqlite3");
|
|
306
|
+
const { drizzle } = await import("drizzle-orm/better-sqlite3");
|
|
307
|
+
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
|
|
308
|
+
const sqliteSchema = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
309
|
+
const rawPath = databaseUrl.startsWith("sqlite://") ? databaseUrl.slice("sqlite://".length) : databaseUrl;
|
|
310
|
+
const filePath = !rawPath ? join3(homedir3(), ".workplane", "workplane.db") : rawPath.startsWith("~/") ? join3(homedir3(), rawPath.slice(2)) : rawPath;
|
|
311
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
312
|
+
const sqlite = new Database2(filePath);
|
|
313
|
+
sqlite.pragma("journal_mode = WAL");
|
|
314
|
+
sqlite.pragma("synchronous = NORMAL");
|
|
315
|
+
const db = drizzle(sqlite, { schema: sqliteSchema });
|
|
316
|
+
migrate(db, { migrationsFolder: resolve(distDir, "migrations/sqlite") });
|
|
317
|
+
sqlite.close();
|
|
318
|
+
process.stdout.write(`SQLite migrations applied: ${filePath}
|
|
319
|
+
`);
|
|
320
|
+
}
|
|
321
|
+
async function runMigration(databaseUrl) {
|
|
322
|
+
if (databaseUrl.startsWith("postgres://") || databaseUrl.startsWith("postgresql://")) {
|
|
323
|
+
await migratePostgres(databaseUrl);
|
|
324
|
+
} else {
|
|
325
|
+
await migrateSqlite(databaseUrl);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ../db/src/migrate.ts
|
|
105
330
|
async function main() {
|
|
106
331
|
const config = loadServerConfig();
|
|
107
|
-
await
|
|
108
|
-
const pool = getPool(config.databaseUrl);
|
|
109
|
-
const filePath = resolve(dirname(fileURLToPath(import.meta.url)), "schema.sql");
|
|
110
|
-
const sql = await readFile(filePath, "utf8");
|
|
111
|
-
await pool.query(sql);
|
|
112
|
-
await pool.end();
|
|
113
|
-
process.stdout.write("Migrations applied.\n");
|
|
332
|
+
await runMigration(config.databaseUrl);
|
|
114
333
|
}
|
|
115
334
|
main().catch((error) => {
|
|
116
335
|
process.stderr.write(`${String(error)}
|
package/dist/migrate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../db/src/migrate.ts","../../core/src/config.ts","../../core/src/env.ts","../../db/src/client.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { loadServerConfig } from \"../../core/src/config.js\";\nimport { getPool } from \"./client.js\";\n\nfunction quoteIdentifier(value: string): string {\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\nfunction parseDatabaseName(databaseUrl: string): string {\n const url = new URL(databaseUrl);\n const name = url.pathname.replace(/^\\//, \"\");\n if (!name) {\n throw new Error(\"DATABASE_URL must include a database name\");\n }\n return name;\n}\n\nasync function ensureDatabaseExists(databaseUrl: string): Promise<void> {\n const databaseName = parseDatabaseName(databaseUrl);\n const adminUrl = new URL(databaseUrl);\n adminUrl.pathname = \"/postgres\";\n const adminPool = getPool(adminUrl.toString());\n\n try {\n const existsResult = await adminPool.query(\"select 1 from pg_database where datname = $1\", [databaseName]);\n if (existsResult.rowCount !== 0) {\n return;\n }\n await adminPool.query(`create database ${quoteIdentifier(databaseName)}`);\n process.stdout.write(`Created database: ${databaseName}\\n`);\n } catch (error) {\n const pgError = error as { code?: string; message?: string };\n if (pgError.code === \"42501\") {\n throw new Error(\n `Database \"${databaseName}\" does not exist and the current role lacks CREATE DATABASE permissions.`,\n );\n }\n throw error;\n } finally {\n await adminPool.end();\n }\n}\n\nasync function main(): Promise<void> {\n const config = loadServerConfig();\n await ensureDatabaseExists(config.databaseUrl);\n const pool = getPool(config.databaseUrl);\n const filePath = resolve(dirname(fileURLToPath(import.meta.url)), \"schema.sql\");\n const sql = await readFile(filePath, \"utf8\");\n await pool.query(sql);\n await pool.end();\n process.stdout.write(\"Migrations applied.\\n\");\n}\n\nmain().catch((error) => {\n process.stderr.write(`${String(error)}\\n`);\n process.exit(1);\n});\n","import { z } from \"zod\";\nimport { loadLocalEnv } from \"./env.js\";\n\nloadLocalEnv();\n\nconst serverEnvSchema = z.object({\n DATABASE_URL: z.string().min(1),\n WORKPLANE_SERVER_PORT: z\n .string()\n .optional()\n .default(\"8787\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_OPERATOR_TOKEN: z.string().optional(),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nconst nodeEnvSchema = z.object({\n WORKPLANE_SERVER_URL: z.string().url().optional().default(\"http://localhost:8787\"),\n WORKPLANE_NODE_NAME: z.string().optional().default(\"local-node-1\"),\n WORKPLANE_NODE_ID: z.string().optional(),\n WORKPLANE_NODE_CAPABILITIES: z\n .string()\n .optional()\n .default(\"shell,git,node,typescript,aider,ollama,codex,claude-code\"),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_POLL_INTERVAL_MS: z\n .string()\n .optional()\n .default(\"3000\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nexport interface ServerConfig {\n databaseUrl: string;\n port: number;\n nodeToken?: string;\n operatorToken?: string;\n envAllowlist: string[];\n}\n\nexport interface NodeConfig {\n serverUrl: string;\n nodeName: string;\n nodeId?: string;\n nodeCapabilities: string[];\n nodeToken?: string;\n pollIntervalMs: number;\n envAllowlist: string[];\n}\n\nexport function loadServerConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {\n const parsed = serverEnvSchema.parse(env);\n\n return {\n databaseUrl: parsed.DATABASE_URL,\n port: parsed.WORKPLANE_SERVER_PORT,\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function loadNodeConfig(env: NodeJS.ProcessEnv = process.env): NodeConfig {\n const parsed = nodeEnvSchema.parse(env);\n\n return {\n serverUrl: parsed.WORKPLANE_SERVER_URL,\n nodeName: parsed.WORKPLANE_NODE_NAME,\n nodeId: parsed.WORKPLANE_NODE_ID,\n nodeCapabilities: parseCsv(parsed.WORKPLANE_NODE_CAPABILITIES),\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n pollIntervalMs: parsed.WORKPLANE_POLL_INTERVAL_MS,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function parseCsv(value: string): string[] {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n}\n","import { config as dotenvConfig } from \"dotenv\";\n\nlet envLoaded = false;\n\nexport function loadLocalEnv(): void {\n if (envLoaded) {\n return;\n }\n\n dotenvConfig({ path: \".env\" });\n dotenvConfig({ path: \".env.local\", override: true });\n\n envLoaded = true;\n}\n\n","import { Pool } from \"pg\";\n\nconst pools = new Map<string, Pool>();\n\nexport function getPool(databaseUrl: string): Pool {\n const existing = pools.get(databaseUrl);\n if (existing) {\n return existing;\n }\n\n const pool = new Pool({ connectionString: databaseUrl });\n pools.set(databaseUrl, pool);\n return pool;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../db/src/schema/sqlite.ts","../../core/src/config.ts","../../core/src/env.ts","../../db/src/migration.ts","../../db/src/client.ts","../../db/src/pg-store.ts","../../db/src/schema/pg.ts","../../db/src/sqlite-store.ts","../../db/src/migrate.ts"],"sourcesContent":["import { isNull, sql } from \"drizzle-orm\";\nimport { index, integer, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\n\nconst nowDefault = sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`;\n\nexport const tasks = sqliteTable(\"tasks\", {\n id: text(\"id\").primaryKey(),\n kind: text(\"kind\").notNull(),\n adapter: text(\"adapter\").notNull(),\n payload: text(\"payload\", { mode: \"json\" }).$type<Record<string, unknown>>().notNull().default({}),\n requires: text(\"requires\", { mode: \"json\" }).$type<string[]>().notNull().default([]),\n status: text(\"status\").notNull(),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n updatedAt: text(\"updated_at\").notNull().default(nowDefault),\n}, (table) => ({\n statusCreatedAtIdx: index(\"idx_tasks_status_created_at\").on(table.status, table.createdAt),\n}));\n\nexport const nodes = sqliteTable(\"nodes\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n capabilities: text(\"capabilities\", { mode: \"json\" }).$type<string[]>().notNull().default([]),\n status: text(\"status\").notNull().default(\"online\"),\n lastHeartbeatAt: text(\"last_heartbeat_at\"),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n updatedAt: text(\"updated_at\").notNull().default(nowDefault),\n});\n\nexport const runs = sqliteTable(\"runs\", {\n id: text(\"id\").primaryKey(),\n taskId: text(\"task_id\").notNull().references(() => tasks.id),\n nodeId: text(\"node_id\").notNull().references(() => nodes.id),\n attempt: integer(\"attempt\").notNull(),\n status: text(\"status\").notNull(),\n startedAt: text(\"started_at\"),\n endedAt: text(\"ended_at\"),\n error: text(\"error\"),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n updatedAt: text(\"updated_at\").notNull().default(nowDefault),\n}, (table) => ({\n taskIdIdx: index(\"idx_runs_task_id\").on(table.taskId),\n nodeIdIdx: index(\"idx_runs_node_id\").on(table.nodeId),\n}));\n\nexport const runLogs = sqliteTable(\"run_logs\", {\n id: integer(\"id\").primaryKey({ autoIncrement: true }),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n stepName: text(\"step_name\"),\n stream: text(\"stream\").notNull(),\n message: text(\"message\").notNull(),\n timestamp: text(\"timestamp\").notNull().default(nowDefault),\n}, (table) => ({\n runIdTimestampIdx: index(\"idx_run_logs_run_id_timestamp\").on(table.runId, table.timestamp),\n}));\n\nexport const artifacts = sqliteTable(\"artifacts\", {\n id: text(\"id\").primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n type: text(\"type\").notNull(),\n name: text(\"name\").notNull(),\n path: text(\"path\").notNull(),\n metadata: text(\"metadata\", { mode: \"json\" }).$type<Record<string, unknown>>(),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n}, (table) => ({\n runIdIdx: index(\"idx_artifacts_run_id\").on(table.runId),\n}));\n\nexport const runInputEvents = sqliteTable(\"run_input_events\", {\n id: integer(\"id\").primaryKey({ autoIncrement: true }),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n sequence: integer(\"sequence\").notNull(),\n kind: text(\"kind\").notNull(),\n payload: text(\"payload\", { mode: \"json\" }).$type<Record<string, unknown>>().notNull().default({}),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n deliveredAt: text(\"delivered_at\"),\n}, (table) => ({\n runSequenceUniqueIdx: uniqueIndex(\"idx_run_input_events_run_sequence\").on(table.runId, table.sequence),\n undeliveredIdx: index(\"idx_run_input_events_undelivered\").on(table.runId, table.sequence).where(isNull(table.deliveredAt)),\n}));\n","import { z } from \"zod\";\nimport { loadLocalEnv } from \"./env.js\";\n\nloadLocalEnv();\n\nconst serverEnvSchema = z.object({\n DATABASE_URL: z.string().optional().default(\"sqlite://~/.workplane/workplane.db\"),\n WORKPLANE_SERVER_PORT: z\n .string()\n .optional()\n .default(\"8787\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_OPERATOR_TOKEN: z.string().optional(),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nconst nodeEnvSchema = z.object({\n WORKPLANE_SERVER_URL: z.string().url().optional().default(\"http://localhost:8787\"),\n WORKPLANE_NODE_NAME: z.string().optional().default(\"local-node-1\"),\n WORKPLANE_NODE_ID: z.string().optional(),\n WORKPLANE_NODE_CAPABILITIES: z\n .string()\n .optional()\n .default(\"shell,git,node,typescript,aider,ollama,codex,claude-code\"),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_POLL_INTERVAL_MS: z\n .string()\n .optional()\n .default(\"3000\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nexport interface ServerConfig {\n databaseUrl: string;\n port: number;\n nodeToken?: string;\n operatorToken?: string;\n envAllowlist: string[];\n}\n\nexport interface NodeConfig {\n serverUrl: string;\n nodeName: string;\n nodeId?: string;\n nodeCapabilities: string[];\n nodeToken?: string;\n pollIntervalMs: number;\n envAllowlist: string[];\n}\n\nexport function loadServerConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {\n const parsed = serverEnvSchema.parse(env);\n\n return {\n databaseUrl: parsed.DATABASE_URL,\n port: parsed.WORKPLANE_SERVER_PORT,\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function loadNodeConfig(env: NodeJS.ProcessEnv = process.env): NodeConfig {\n const parsed = nodeEnvSchema.parse(env);\n\n return {\n serverUrl: parsed.WORKPLANE_SERVER_URL,\n nodeName: parsed.WORKPLANE_NODE_NAME,\n nodeId: parsed.WORKPLANE_NODE_ID,\n nodeCapabilities: parseCsv(parsed.WORKPLANE_NODE_CAPABILITIES),\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n pollIntervalMs: parsed.WORKPLANE_POLL_INTERVAL_MS,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function parseCsv(value: string): string[] {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n}\n","import { config as dotenvConfig } from \"dotenv\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nlet envLoaded = false;\n\n// Load order (highest priority first, all with override:false so shell env vars win):\n// 1. .env.local — project-local overrides (gitignored)\n// 2. ~/.workplane/.env — user global config written by workplane-setup\n// 3. .env — project defaults (committed)\nexport function loadLocalEnv(): void {\n if (envLoaded) {\n return;\n }\n\n dotenvConfig({ path: \".env.local\" });\n dotenvConfig({ path: join(homedir(), \".workplane\", \".env\") });\n dotenvConfig({ path: \".env\" });\n\n envLoaded = true;\n}\n","// Side-effect-free module — safe to import from both the migrate CLI and setup wizard.\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getDrizzle, getPool } from \"./client.js\";\n\nconst distDir = dirname(fileURLToPath(import.meta.url));\n\nasync function migratePostgres(databaseUrl: string): Promise<void> {\n const adminUrl = new URL(databaseUrl);\n const dbName = adminUrl.pathname.replace(/^\\//, \"\");\n if (!dbName) throw new Error(\"DATABASE_URL must include a database name\");\n\n adminUrl.pathname = \"/postgres\";\n const adminPool = getPool(adminUrl.toString());\n try {\n const { rowCount } = await adminPool.query(\n \"SELECT 1 FROM pg_database WHERE datname = $1\",\n [dbName],\n );\n if (!rowCount) {\n await adminPool.query(`CREATE DATABASE \"${dbName.replaceAll('\"', '\"\"')}\"`);\n process.stdout.write(`Created database: ${dbName}\\n`);\n }\n } catch (err) {\n const e = err as { code?: string };\n if (e.code === \"42501\") {\n throw new Error(\n `Database \"${dbName}\" does not exist and the current role lacks CREATE DATABASE permissions.`,\n );\n }\n throw err;\n } finally {\n await adminPool.end();\n }\n\n const pool = getPool(databaseUrl);\n const { migrate } = await import(\"drizzle-orm/node-postgres/migrator\");\n await migrate(getDrizzle(pool), { migrationsFolder: resolve(distDir, \"migrations/pg\") });\n await pool.end();\n process.stdout.write(\"Postgres migrations applied.\\n\");\n}\n\nasync function migrateSqlite(databaseUrl: string): Promise<void> {\n const { default: Database } = await import(\"better-sqlite3\");\n const { drizzle } = await import(\"drizzle-orm/better-sqlite3\");\n const { migrate } = await import(\"drizzle-orm/better-sqlite3/migrator\");\n const sqliteSchema = await import(\"./schema/sqlite.js\");\n\n const rawPath = databaseUrl.startsWith(\"sqlite://\")\n ? databaseUrl.slice(\"sqlite://\".length)\n : databaseUrl;\n const filePath = !rawPath\n ? join(homedir(), \".workplane\", \"workplane.db\")\n : rawPath.startsWith(\"~/\") ? join(homedir(), rawPath.slice(2)) : rawPath;\n\n mkdirSync(dirname(filePath), { recursive: true });\n const sqlite = new Database(filePath);\n sqlite.pragma(\"journal_mode = WAL\");\n sqlite.pragma(\"synchronous = NORMAL\");\n const db = drizzle(sqlite, { schema: sqliteSchema });\n migrate(db, { migrationsFolder: resolve(distDir, \"migrations/sqlite\") });\n sqlite.close();\n process.stdout.write(`SQLite migrations applied: ${filePath}\\n`);\n}\n\nexport async function runMigration(databaseUrl: string): Promise<void> {\n if (databaseUrl.startsWith(\"postgres://\") || databaseUrl.startsWith(\"postgresql://\")) {\n await migratePostgres(databaseUrl);\n } else {\n await migrateSqlite(databaseUrl);\n }\n}\n","import { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { drizzle as drizzlePg } from \"drizzle-orm/node-postgres\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { drizzle as drizzleSqlite } from \"drizzle-orm/better-sqlite3\";\nimport Database from \"better-sqlite3\";\nimport { Pool } from \"pg\";\nimport { PgStore } from \"./pg-store.js\";\nimport { SqliteStore } from \"./sqlite-store.js\";\nimport * as pgSchema from \"./schema/pg.js\";\nimport * as sqliteSchema from \"./schema/sqlite.js\";\nimport type { WorkplaneStore } from \"./store-interface.js\";\n\nexport type DrizzleDb = NodePgDatabase<typeof pgSchema>;\n\nexport interface StoreHandle {\n store: WorkplaneStore;\n close: () => Promise<void>;\n}\n\nconst pools = new Map<string, Pool>();\n\nexport function getPool(databaseUrl: string): Pool {\n const existing = pools.get(databaseUrl);\n if (existing) return existing;\n const pool = new Pool({ connectionString: databaseUrl });\n pools.set(databaseUrl, pool);\n return pool;\n}\n\nexport function getDrizzle(pool: Pool): DrizzleDb {\n return drizzlePg(pool, { schema: pgSchema });\n}\n\nfunction parseSqlitePath(url: string): string {\n const path = url.startsWith(\"sqlite://\") ? url.slice(\"sqlite://\".length) : url;\n if (!path) return join(homedir(), \".workplane\", \"workplane.db\");\n if (path.startsWith(\"~/\")) return join(homedir(), path.slice(2));\n return path;\n}\n\nexport function createStore(databaseUrl?: string): StoreHandle {\n const url = databaseUrl ?? \"\";\n\n if (url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\")) {\n const pool = getPool(url);\n return {\n store: new PgStore(getDrizzle(pool)),\n close: () => pool.end(),\n };\n }\n\n const filePath = parseSqlitePath(url);\n mkdirSync(dirname(filePath), { recursive: true });\n const sqlite = new Database(filePath);\n sqlite.pragma(\"journal_mode = WAL\");\n sqlite.pragma(\"synchronous = NORMAL\");\n const db = drizzleSqlite(sqlite, { schema: sqliteSchema });\n return {\n store: new SqliteStore(db),\n close: async () => { sqlite.close(); },\n };\n}\n","import { and, asc, desc, eq, gt, sql } from \"drizzle-orm\";\nimport { makeId } from \"../../core/src/ids.js\";\nimport type { DrizzleDb } from \"./client.js\";\nimport { artifacts, nodes, runInputEvents, runLogs, runs, tasks } from \"./schema/pg.js\";\nimport type { WorkplaneStore } from \"./store-interface.js\";\nimport type {\n AppendInputEventInput,\n ArtifactInput,\n ArtifactRecord,\n CreateTaskInput,\n NodePollResult,\n NodeRecord,\n RunInputEvent,\n RunLogInput,\n RunLogRecord,\n RunRecord,\n TaskRecord,\n} from \"../../types/src/index.js\";\n\nfunction toTaskRecord(row: typeof tasks.$inferSelect): TaskRecord {\n return {\n id: row.id,\n kind: row.kind,\n adapter: row.adapter,\n payload: row.payload,\n requires: row.requires ?? [],\n status: row.status as TaskRecord[\"status\"],\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n };\n}\n\nfunction toRunRecord(row: typeof runs.$inferSelect): RunRecord {\n return {\n id: row.id,\n taskId: row.taskId,\n nodeId: row.nodeId,\n attempt: row.attempt,\n status: row.status as RunRecord[\"status\"],\n startedAt: row.startedAt?.toISOString() ?? null,\n endedAt: row.endedAt?.toISOString() ?? null,\n error: row.error ?? null,\n };\n}\n\nfunction toNodeRecord(row: typeof nodes.$inferSelect): NodeRecord {\n return {\n id: row.id,\n name: row.name,\n capabilities: row.capabilities ?? [],\n status: row.status as NodeRecord[\"status\"],\n lastHeartbeatAt: row.lastHeartbeatAt?.toISOString() ?? null,\n };\n}\n\nfunction toLogRecord(row: typeof runLogs.$inferSelect): RunLogRecord {\n return {\n id: row.id,\n runId: row.runId,\n stepName: row.stepName ?? null,\n stream: row.stream as RunLogRecord[\"stream\"],\n message: row.message,\n timestamp: row.timestamp.toISOString(),\n };\n}\n\nfunction toArtifactRecord(row: typeof artifacts.$inferSelect): ArtifactRecord {\n return {\n id: row.id,\n runId: row.runId,\n type: row.type,\n name: row.name,\n path: row.path,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt.toISOString(),\n };\n}\n\nfunction toInputEventRecord(row: typeof runInputEvents.$inferSelect): RunInputEvent {\n return {\n id: row.id,\n runId: row.runId,\n sequence: row.sequence,\n kind: row.kind as RunInputEvent[\"kind\"],\n payload: row.payload,\n createdAt: row.createdAt.toISOString(),\n deliveredAt: row.deliveredAt?.toISOString() ?? null,\n };\n}\n\nfunction mapRawTask(row: Record<string, unknown>): TaskRecord {\n return {\n id: String(row.id),\n kind: String(row.kind),\n adapter: String(row.adapter),\n payload: (row.payload as Record<string, unknown>) ?? {},\n requires: (row.requires as string[]) ?? [],\n status: row.status as TaskRecord[\"status\"],\n createdAt: new Date(String(row.created_at)).toISOString(),\n updatedAt: new Date(String(row.updated_at)).toISOString(),\n };\n}\n\nfunction mapRawRun(row: Record<string, unknown>): RunRecord {\n return {\n id: String(row.id),\n taskId: String(row.task_id),\n nodeId: String(row.node_id),\n attempt: Number(row.attempt),\n status: row.status as RunRecord[\"status\"],\n startedAt: row.started_at ? new Date(String(row.started_at)).toISOString() : null,\n endedAt: row.ended_at ? new Date(String(row.ended_at)).toISOString() : null,\n error: row.error ? String(row.error) : null,\n };\n}\n\nexport class PgStore implements WorkplaneStore {\n constructor(private readonly db: DrizzleDb) {}\n\n async createTask(input: CreateTaskInput): Promise<TaskRecord> {\n const taskId = makeId(\"task\");\n const [row] = await this.db.insert(tasks).values({\n id: taskId,\n kind: input.kind,\n adapter: input.adapter,\n payload: input.payload,\n requires: input.requires ?? [],\n status: \"queued\",\n }).returning();\n return toTaskRecord(row);\n }\n\n async listTasks(status?: TaskRecord[\"status\"]): Promise<TaskRecord[]> {\n const rows = await this.db\n .select()\n .from(tasks)\n .where(status ? eq(tasks.status, status) : undefined)\n .orderBy(desc(tasks.createdAt))\n .limit(100);\n return rows.map(toTaskRecord);\n }\n\n async getTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.select().from(tasks).where(eq(tasks.id, taskId));\n return row ? toTaskRecord(row) : null;\n }\n\n async listRuns(filters?: { taskId?: string; status?: RunRecord[\"status\"] }): Promise<RunRecord[]> {\n const conditions = [];\n if (filters?.taskId) conditions.push(eq(runs.taskId, filters.taskId));\n if (filters?.status) conditions.push(eq(runs.status, filters.status));\n\n const rows = await this.db\n .select()\n .from(runs)\n .where(conditions.length ? and(...conditions) : undefined)\n .orderBy(desc(runs.createdAt))\n .limit(100);\n return rows.map(toRunRecord);\n }\n\n async getRun(runId: string): Promise<RunRecord | null> {\n const [row] = await this.db.select().from(runs).where(eq(runs.id, runId));\n return row ? toRunRecord(row) : null;\n }\n\n async registerNode(name: string, capabilities: string[], preferredId?: string): Promise<NodeRecord> {\n const [existing] = await this.db.select().from(nodes).where(eq(nodes.name, name));\n\n if (existing) {\n const [updated] = await this.db.update(nodes)\n .set({ capabilities, status: \"online\", lastHeartbeatAt: sql`now()`, updatedAt: sql`now()` })\n .where(eq(nodes.id, existing.id))\n .returning();\n return toNodeRecord(updated);\n }\n\n const nodeId = preferredId ?? makeId(\"node\");\n const [inserted] = await this.db.insert(nodes).values({\n id: nodeId, name, capabilities, status: \"online\", lastHeartbeatAt: sql`now()`,\n }).returning();\n return toNodeRecord(inserted);\n }\n\n async getRunCancellationState(runId: string): Promise<{ runStatus: string; taskStatus: string } | null> {\n const [row] = await this.db\n .select({ runStatus: runs.status, taskStatus: tasks.status })\n .from(runs)\n .innerJoin(tasks, eq(tasks.id, runs.taskId))\n .where(eq(runs.id, runId));\n return row ?? null;\n }\n\n async pollNode(nodeId: string, capabilities: string[]): Promise<NodePollResult | null> {\n return await this.db.transaction(async (tx) => {\n await tx.execute(sql`\n UPDATE nodes\n SET capabilities = ${capabilities}::text[], status = 'online',\n last_heartbeat_at = now(), updated_at = now()\n WHERE id = ${nodeId}\n `);\n\n const taskResult = await tx.execute(sql`\n SELECT * FROM tasks\n WHERE status = 'queued' AND requires <@ ${capabilities}::text[]\n ORDER BY created_at ASC\n LIMIT 1\n FOR UPDATE SKIP LOCKED\n `);\n\n if (taskResult.rows.length === 0) return null;\n\n const taskRow = taskResult.rows[0] as Record<string, unknown>;\n const taskId = String(taskRow.id);\n\n const attemptResult = await tx.execute(sql`\n SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ${taskId}\n `);\n const attempt = Number((attemptResult.rows[0] as Record<string, unknown>).max_attempt) + 1;\n\n const runId = makeId(\"run\");\n\n const runResult = await tx.execute(sql`\n INSERT INTO runs (id, task_id, node_id, attempt, status, started_at)\n VALUES (${runId}, ${taskId}, ${nodeId}, ${attempt}, 'assigned', now())\n RETURNING *\n `);\n\n await tx.execute(sql`\n UPDATE tasks SET status = 'assigned', updated_at = now() WHERE id = ${taskId}\n `);\n\n return {\n task: mapRawTask(taskRow),\n run: mapRawRun(runResult.rows[0] as Record<string, unknown>),\n };\n });\n }\n\n async updateRunStatus(runId: string, status: RunRecord[\"status\"], error?: string): Promise<RunRecord | null> {\n return await this.db.transaction(async (tx) => {\n const currentResult = await tx.execute(sql`\n SELECT r.*, t.status AS task_status\n FROM runs r JOIN tasks t ON t.id = r.task_id\n WHERE r.id = ${runId}\n FOR UPDATE\n `);\n\n if (currentResult.rows.length === 0) return null;\n\n const currentRow = currentResult.rows[0] as Record<string, unknown>;\n const currentTaskStatus = String(currentRow.task_status);\n\n if (currentTaskStatus === \"cancelled\" && status !== \"cancelled\") {\n return mapRawRun(currentRow);\n }\n\n const isTerminal = status === \"succeeded\" || status === \"failed\" || status === \"cancelled\";\n const [updatedRun] = await tx.update(runs)\n .set(isTerminal\n ? { status, error: error ?? null, endedAt: sql`now()`, updatedAt: sql`now()` }\n : { status, error: error ?? null, updatedAt: sql`now()` })\n .where(eq(runs.id, runId))\n .returning();\n\n if (!updatedRun) return null;\n\n const nextTaskStatus = isTerminal ? status : \"running\";\n if (currentTaskStatus !== \"cancelled\" || status === \"cancelled\") {\n await tx.update(tasks)\n .set({ status: nextTaskStatus, updatedAt: sql`now()` })\n .where(eq(tasks.id, updatedRun.taskId));\n }\n\n return toRunRecord(updatedRun);\n });\n }\n\n async appendRunLogs(runId: string, logs: RunLogInput[]): Promise<number> {\n if (logs.length === 0) return 0;\n await this.db.insert(runLogs).values(\n logs.map((log) => ({\n runId,\n stepName: log.stepName ?? null,\n stream: log.stream,\n message: log.message,\n })),\n );\n return logs.length;\n }\n\n async getRunLogs(runId: string): Promise<RunLogRecord[]> {\n const rows = await this.db\n .select()\n .from(runLogs)\n .where(eq(runLogs.runId, runId))\n .orderBy(asc(runLogs.timestamp));\n return rows.map(toLogRecord);\n }\n\n async createArtifact(runId: string, input: ArtifactInput): Promise<ArtifactRecord> {\n const artifactId = makeId(\"artifact\");\n const [row] = await this.db.insert(artifacts).values({\n id: artifactId,\n runId,\n type: input.type,\n name: input.name,\n path: input.path,\n metadata: input.metadata ?? null,\n }).returning();\n return toArtifactRecord(row);\n }\n\n async listRunArtifacts(runId: string): Promise<ArtifactRecord[]> {\n const rows = await this.db\n .select()\n .from(artifacts)\n .where(eq(artifacts.runId, runId))\n .orderBy(asc(artifacts.createdAt));\n return rows.map(toArtifactRecord);\n }\n\n async retryTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.update(tasks)\n .set({ status: \"queued\", updatedAt: sql`now()` })\n .where(and(eq(tasks.id, taskId), eq(tasks.status, \"failed\")))\n .returning();\n return row ? toTaskRecord(row) : null;\n }\n\n async cancelTask(taskId: string): Promise<TaskRecord | null> {\n return await this.db.transaction(async (tx) => {\n const taskResult = await tx.execute(sql`\n SELECT * FROM tasks WHERE id = ${taskId} FOR UPDATE\n `);\n\n if (taskResult.rows.length === 0) return null;\n\n const taskRow = taskResult.rows[0] as Record<string, unknown>;\n const taskStatus = String(taskRow.status);\n\n if (taskStatus === \"succeeded\" || taskStatus === \"failed\" || taskStatus === \"cancelled\") {\n return null;\n }\n\n if (taskStatus === \"assigned\" || taskStatus === \"running\") {\n const runResult = await tx.execute(sql`\n SELECT * FROM runs WHERE task_id = ${taskId} ORDER BY attempt DESC LIMIT 1 FOR UPDATE\n `);\n\n if (runResult.rows.length > 0) {\n const runRow = runResult.rows[0] as Record<string, unknown>;\n const runStatus = String(runRow.status);\n if (runStatus !== \"succeeded\" && runStatus !== \"failed\" && runStatus !== \"cancelled\") {\n await tx.execute(sql`\n UPDATE runs SET status = 'cancelled', ended_at = now(), updated_at = now(),\n error = COALESCE(error, 'task cancelled')\n WHERE id = ${runRow.id}\n `);\n }\n }\n }\n\n const [updatedTask] = await tx.update(tasks)\n .set({ status: \"cancelled\", updatedAt: sql`now()` })\n .where(eq(tasks.id, taskId))\n .returning();\n\n return toTaskRecord(updatedTask);\n });\n }\n\n async appendInputEvent(runId: string, input: AppendInputEventInput): Promise<RunInputEvent> {\n return await this.db.transaction(async (tx) => {\n const seqResult = await tx.execute(sql`\n SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM run_input_events WHERE run_id = ${runId}\n `);\n const sequence = Number((seqResult.rows[0] as Record<string, unknown>).next_seq);\n\n const [inserted] = await tx.insert(runInputEvents)\n .values({ runId, sequence, kind: input.kind, payload: input.payload })\n .returning();\n\n return toInputEventRecord(inserted);\n });\n }\n\n async getInputEvents(runId: string, afterSequence: number): Promise<RunInputEvent[]> {\n const rows = await this.db\n .select()\n .from(runInputEvents)\n .where(and(eq(runInputEvents.runId, runId), gt(runInputEvents.sequence, afterSequence)))\n .orderBy(asc(runInputEvents.sequence));\n return rows.map(toInputEventRecord);\n }\n\n async markInputDelivered(runId: string, sequence: number): Promise<void> {\n await this.db.update(runInputEvents)\n .set({ deliveredAt: sql`now()` })\n .where(and(eq(runInputEvents.runId, runId), eq(runInputEvents.sequence, sequence)));\n }\n}\n","import { isNull, sql } from \"drizzle-orm\";\nimport { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from \"drizzle-orm/pg-core\";\n\nexport const tasks = pgTable(\"tasks\", {\n id: text(\"id\").primaryKey(),\n kind: text(\"kind\").notNull(),\n adapter: text(\"adapter\").notNull(),\n payload: jsonb(\"payload\").$type<Record<string, unknown>>().notNull(),\n requires: text(\"requires\").array().notNull().default(sql`'{}'::text[]`),\n status: text(\"status\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n statusCreatedAtIdx: index(\"idx_tasks_status_created_at\").on(table.status, table.createdAt),\n}));\n\nexport const nodes = pgTable(\"nodes\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n capabilities: text(\"capabilities\").array().notNull().default(sql`'{}'::text[]`),\n status: text(\"status\").notNull().default(\"online\"),\n lastHeartbeatAt: timestamp(\"last_heartbeat_at\", { withTimezone: true }),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport const runs = pgTable(\"runs\", {\n id: text(\"id\").primaryKey(),\n taskId: text(\"task_id\").notNull().references(() => tasks.id),\n nodeId: text(\"node_id\").notNull().references(() => nodes.id),\n attempt: integer(\"attempt\").notNull(),\n status: text(\"status\").notNull(),\n startedAt: timestamp(\"started_at\", { withTimezone: true }),\n endedAt: timestamp(\"ended_at\", { withTimezone: true }),\n error: text(\"error\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n taskIdIdx: index(\"idx_runs_task_id\").on(table.taskId),\n nodeIdIdx: index(\"idx_runs_node_id\").on(table.nodeId),\n}));\n\nexport const runLogs = pgTable(\"run_logs\", {\n id: bigserial(\"id\", { mode: \"number\" }).primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n stepName: text(\"step_name\"),\n stream: text(\"stream\").notNull(),\n message: text(\"message\").notNull(),\n timestamp: timestamp(\"timestamp\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n runIdTimestampIdx: index(\"idx_run_logs_run_id_timestamp\").on(table.runId, table.timestamp),\n}));\n\nexport const artifacts = pgTable(\"artifacts\", {\n id: text(\"id\").primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n type: text(\"type\").notNull(),\n name: text(\"name\").notNull(),\n path: text(\"path\").notNull(),\n metadata: jsonb(\"metadata\").$type<Record<string, unknown>>(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n runIdIdx: index(\"idx_artifacts_run_id\").on(table.runId),\n}));\n\nexport const runInputEvents = pgTable(\"run_input_events\", {\n id: bigserial(\"id\", { mode: \"number\" }).primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n sequence: bigint(\"sequence\", { mode: \"number\" }).notNull(),\n kind: text(\"kind\").notNull(),\n payload: jsonb(\"payload\").$type<Record<string, unknown>>().notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n deliveredAt: timestamp(\"delivered_at\", { withTimezone: true }),\n}, (table) => ({\n runSequenceUniqueIdx: uniqueIndex(\"idx_run_input_events_run_sequence\").on(table.runId, table.sequence),\n undeliveredIdx: index(\"idx_run_input_events_undelivered\").on(table.runId, table.sequence).where(isNull(table.deliveredAt)),\n}));\n","import { and, asc, desc, eq, gt, sql } from \"drizzle-orm\";\nimport type { BetterSQLite3Database } from \"drizzle-orm/better-sqlite3\";\nimport { makeId } from \"../../core/src/ids.js\";\nimport { artifacts, nodes, runInputEvents, runLogs, runs, tasks } from \"./schema/sqlite.js\";\nimport type * as sqliteSchema from \"./schema/sqlite.js\";\nimport type { WorkplaneStore } from \"./store-interface.js\";\nimport type {\n AppendInputEventInput,\n ArtifactInput,\n ArtifactRecord,\n CreateTaskInput,\n NodePollResult,\n NodeRecord,\n RunInputEvent,\n RunLogInput,\n RunLogRecord,\n RunRecord,\n TaskRecord,\n} from \"../../types/src/index.js\";\n\ntype SqliteDb = BetterSQLite3Database<typeof sqliteSchema>;\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\n// Drizzle-typed mappers for simple CRUD methods (camelCase, text timestamps)\n\nfunction toTaskRecord(row: typeof tasks.$inferSelect): TaskRecord {\n return {\n id: row.id,\n kind: row.kind,\n adapter: row.adapter,\n payload: row.payload,\n requires: row.requires ?? [],\n status: row.status as TaskRecord[\"status\"],\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction toRunRecord(row: typeof runs.$inferSelect): RunRecord {\n return {\n id: row.id,\n taskId: row.taskId,\n nodeId: row.nodeId,\n attempt: row.attempt,\n status: row.status as RunRecord[\"status\"],\n startedAt: row.startedAt ?? null,\n endedAt: row.endedAt ?? null,\n error: row.error ?? null,\n };\n}\n\nfunction toNodeRecord(row: typeof nodes.$inferSelect): NodeRecord {\n return {\n id: row.id,\n name: row.name,\n capabilities: row.capabilities ?? [],\n status: row.status as NodeRecord[\"status\"],\n lastHeartbeatAt: row.lastHeartbeatAt ?? null,\n };\n}\n\nfunction toLogRecord(row: typeof runLogs.$inferSelect): RunLogRecord {\n return {\n id: row.id,\n runId: row.runId,\n stepName: row.stepName ?? null,\n stream: row.stream as RunLogRecord[\"stream\"],\n message: row.message,\n timestamp: row.timestamp,\n };\n}\n\nfunction toArtifactRecord(row: typeof artifacts.$inferSelect): ArtifactRecord {\n return {\n id: row.id,\n runId: row.runId,\n type: row.type,\n name: row.name,\n path: row.path,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt,\n };\n}\n\nfunction toInputEventRecord(row: typeof runInputEvents.$inferSelect): RunInputEvent {\n return {\n id: row.id,\n runId: row.runId,\n sequence: row.sequence,\n kind: row.kind as RunInputEvent[\"kind\"],\n payload: row.payload,\n createdAt: row.createdAt,\n deliveredAt: row.deliveredAt ?? null,\n };\n}\n\n// Raw-row mappers for transaction methods that use $client directly.\n// better-sqlite3 rejects async transaction callbacks so we use the sync\n// $client API with prepared statements for the 4 methods that need atomicity.\n\ninterface RawTaskRow {\n id: string; kind: string; adapter: string;\n payload: string; requires: string; status: string;\n created_at: string; updated_at: string;\n}\n\ninterface RawRunRow {\n id: string; task_id: string; node_id: string; attempt: number; status: string;\n started_at: string | null; ended_at: string | null; error: string | null;\n created_at: string; updated_at: string;\n}\n\nfunction mapRawTask(row: RawTaskRow): TaskRecord {\n return {\n id: row.id, kind: row.kind, adapter: row.adapter,\n payload: JSON.parse(row.payload) as Record<string, unknown>,\n requires: JSON.parse(row.requires) as string[],\n status: row.status as TaskRecord[\"status\"],\n createdAt: row.created_at, updatedAt: row.updated_at,\n };\n}\n\nfunction mapRawRun(row: RawRunRow): RunRecord {\n return {\n id: row.id, taskId: row.task_id, nodeId: row.node_id,\n attempt: row.attempt, status: row.status as RunRecord[\"status\"],\n startedAt: row.started_at ?? null, endedAt: row.ended_at ?? null,\n error: row.error ?? null,\n };\n}\n\nexport class SqliteStore implements WorkplaneStore {\n constructor(private readonly db: SqliteDb) {}\n\n async createTask(input: CreateTaskInput): Promise<TaskRecord> {\n const now = nowIso();\n const [row] = await this.db.insert(tasks).values({\n id: makeId(\"task\"),\n kind: input.kind,\n adapter: input.adapter,\n payload: input.payload,\n requires: input.requires ?? [],\n status: \"queued\",\n createdAt: now,\n updatedAt: now,\n }).returning();\n return toTaskRecord(row);\n }\n\n async listTasks(status?: TaskRecord[\"status\"]): Promise<TaskRecord[]> {\n const rows = await this.db.select().from(tasks)\n .where(status ? eq(tasks.status, status) : undefined)\n .orderBy(desc(tasks.createdAt)).limit(100);\n return rows.map(toTaskRecord);\n }\n\n async getTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.select().from(tasks).where(eq(tasks.id, taskId));\n return row ? toTaskRecord(row) : null;\n }\n\n async listRuns(filters?: { taskId?: string; status?: RunRecord[\"status\"] }): Promise<RunRecord[]> {\n const conds = [];\n if (filters?.taskId) conds.push(eq(runs.taskId, filters.taskId));\n if (filters?.status) conds.push(eq(runs.status, filters.status));\n const rows = await this.db.select().from(runs)\n .where(conds.length ? and(...conds) : undefined)\n .orderBy(desc(runs.createdAt)).limit(100);\n return rows.map(toRunRecord);\n }\n\n async getRun(runId: string): Promise<RunRecord | null> {\n const [row] = await this.db.select().from(runs).where(eq(runs.id, runId));\n return row ? toRunRecord(row) : null;\n }\n\n async registerNode(name: string, capabilities: string[], preferredId?: string): Promise<NodeRecord> {\n const now = nowIso();\n const [existing] = await this.db.select().from(nodes).where(eq(nodes.name, name));\n if (existing) {\n const [updated] = await this.db.update(nodes)\n .set({ capabilities, status: \"online\", lastHeartbeatAt: now, updatedAt: now })\n .where(eq(nodes.id, existing.id)).returning();\n return toNodeRecord(updated);\n }\n const [inserted] = await this.db.insert(nodes).values({\n id: preferredId ?? makeId(\"node\"), name, capabilities,\n status: \"online\", lastHeartbeatAt: now, createdAt: now, updatedAt: now,\n }).returning();\n return toNodeRecord(inserted);\n }\n\n async getRunCancellationState(runId: string): Promise<{ runStatus: string; taskStatus: string } | null> {\n const [row] = await this.db\n .select({ runStatus: runs.status, taskStatus: tasks.status })\n .from(runs).innerJoin(tasks, eq(tasks.id, runs.taskId)).where(eq(runs.id, runId));\n return row ? { runStatus: row.runStatus, taskStatus: row.taskStatus ?? \"\" } : null;\n }\n\n // Transaction methods use this.db.$client directly because better-sqlite3\n // rejects async callbacks — all operations inside must be synchronous.\n\n async pollNode(nodeId: string, capabilities: string[]): Promise<NodePollResult | null> {\n const now = nowIso();\n await this.db.update(nodes)\n .set({ capabilities, status: \"online\", lastHeartbeatAt: now, updatedAt: now })\n .where(eq(nodes.id, nodeId));\n\n const raw = this.db.$client;\n return raw.transaction(() => {\n const capSet = new Set(capabilities);\n const queued = raw.prepare(\n \"SELECT * FROM tasks WHERE status = 'queued' ORDER BY created_at ASC\",\n ).all() as RawTaskRow[];\n\n const task = queued.find((t) =>\n (JSON.parse(t.requires) as string[]).every((r) => capSet.has(r)),\n );\n if (!task) return null;\n\n const { max_attempt } = raw.prepare(\n \"SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ?\",\n ).get(task.id) as { max_attempt: number };\n\n const runId = makeId(\"run\");\n raw.prepare(\n \"INSERT INTO runs (id, task_id, node_id, attempt, status, started_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'assigned', ?, ?, ?)\",\n ).run(runId, task.id, nodeId, max_attempt + 1, now, now, now);\n\n raw.prepare(\"UPDATE tasks SET status = 'assigned', updated_at = ? WHERE id = ?\").run(now, task.id);\n\n return {\n task: mapRawTask(task),\n run: mapRawRun(raw.prepare(\"SELECT * FROM runs WHERE id = ?\").get(runId) as RawRunRow),\n };\n })();\n }\n\n async updateRunStatus(runId: string, status: RunRecord[\"status\"], error?: string): Promise<RunRecord | null> {\n const raw = this.db.$client;\n return raw.transaction(() => {\n const current = raw.prepare(\n \"SELECT r.*, t.status AS task_status FROM runs r JOIN tasks t ON t.id = r.task_id WHERE r.id = ?\",\n ).get(runId) as (RawRunRow & { task_status: string }) | undefined;\n if (!current) return null;\n\n if (current.task_status === \"cancelled\" && status !== \"cancelled\") {\n return mapRawRun(current);\n }\n\n const now = nowIso();\n const isTerminal = status === \"succeeded\" || status === \"failed\" || status === \"cancelled\";\n if (isTerminal) {\n raw.prepare(\n \"UPDATE runs SET status = ?, error = ?, ended_at = ?, updated_at = ? WHERE id = ?\",\n ).run(status, error ?? null, now, now, runId);\n } else {\n raw.prepare(\n \"UPDATE runs SET status = ?, error = ?, updated_at = ? WHERE id = ?\",\n ).run(status, error ?? null, now, runId);\n }\n\n const updatedRun = raw.prepare(\"SELECT * FROM runs WHERE id = ?\").get(runId) as RawRunRow;\n const nextTaskStatus = isTerminal ? status : \"running\";\n if (current.task_status !== \"cancelled\" || status === \"cancelled\") {\n raw.prepare(\"UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?\").run(\n nextTaskStatus, now, updatedRun.task_id,\n );\n }\n return mapRawRun(updatedRun);\n })();\n }\n\n async appendRunLogs(runId: string, logs: RunLogInput[]): Promise<number> {\n if (logs.length === 0) return 0;\n const now = nowIso();\n await this.db.insert(runLogs).values(\n logs.map((log) => ({\n runId, stepName: log.stepName ?? null,\n stream: log.stream, message: log.message, timestamp: now,\n })),\n );\n return logs.length;\n }\n\n async getRunLogs(runId: string): Promise<RunLogRecord[]> {\n const rows = await this.db.select().from(runLogs)\n .where(eq(runLogs.runId, runId)).orderBy(asc(runLogs.timestamp));\n return rows.map(toLogRecord);\n }\n\n async createArtifact(runId: string, input: ArtifactInput): Promise<ArtifactRecord> {\n const now = nowIso();\n const [row] = await this.db.insert(artifacts).values({\n id: makeId(\"artifact\"), runId, type: input.type, name: input.name,\n path: input.path, metadata: input.metadata ?? null, createdAt: now,\n }).returning();\n return toArtifactRecord(row);\n }\n\n async listRunArtifacts(runId: string): Promise<ArtifactRecord[]> {\n const rows = await this.db.select().from(artifacts)\n .where(eq(artifacts.runId, runId)).orderBy(asc(artifacts.createdAt));\n return rows.map(toArtifactRecord);\n }\n\n async retryTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.update(tasks)\n .set({ status: \"queued\", updatedAt: nowIso() })\n .where(and(eq(tasks.id, taskId), eq(tasks.status, \"failed\"))).returning();\n return row ? toTaskRecord(row) : null;\n }\n\n async cancelTask(taskId: string): Promise<TaskRecord | null> {\n const raw = this.db.$client;\n return raw.transaction(() => {\n const taskRow = raw.prepare(\"SELECT * FROM tasks WHERE id = ?\").get(taskId) as RawTaskRow | undefined;\n if (!taskRow) return null;\n\n const terminal = new Set([\"succeeded\", \"failed\", \"cancelled\"]);\n if (terminal.has(taskRow.status)) return null;\n\n const now = nowIso();\n if (taskRow.status === \"assigned\" || taskRow.status === \"running\") {\n const latestRun = raw.prepare(\n \"SELECT * FROM runs WHERE task_id = ? ORDER BY attempt DESC LIMIT 1\",\n ).get(taskId) as RawRunRow | undefined;\n\n if (latestRun && !terminal.has(latestRun.status)) {\n raw.prepare(\n \"UPDATE runs SET status = 'cancelled', ended_at = ?, updated_at = ?, error = COALESCE(error, 'task cancelled') WHERE id = ?\",\n ).run(now, now, latestRun.id);\n }\n }\n\n raw.prepare(\"UPDATE tasks SET status = 'cancelled', updated_at = ? WHERE id = ?\").run(now, taskId);\n return mapRawTask(raw.prepare(\"SELECT * FROM tasks WHERE id = ?\").get(taskId) as RawTaskRow);\n })();\n }\n\n async appendInputEvent(runId: string, input: AppendInputEventInput): Promise<RunInputEvent> {\n const raw = this.db.$client;\n const now = nowIso();\n const event = raw.transaction(() => {\n const { next_seq } = raw.prepare(\n \"SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM run_input_events WHERE run_id = ?\",\n ).get(runId) as { next_seq: number };\n\n raw.prepare(\n \"INSERT INTO run_input_events (run_id, sequence, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)\",\n ).run(runId, next_seq, input.kind, JSON.stringify(input.payload), now);\n\n return raw.prepare(\n \"SELECT * FROM run_input_events WHERE run_id = ? AND sequence = ?\",\n ).get(runId, next_seq) as {\n id: number; run_id: string; sequence: number; kind: string;\n payload: string; created_at: string; delivered_at: string | null;\n };\n })();\n\n return {\n id: event.id, runId: event.run_id, sequence: event.sequence,\n kind: event.kind as RunInputEvent[\"kind\"],\n payload: JSON.parse(event.payload) as Record<string, unknown>,\n createdAt: event.created_at, deliveredAt: event.delivered_at ?? null,\n };\n }\n\n async getInputEvents(runId: string, afterSequence: number): Promise<RunInputEvent[]> {\n const rows = await this.db.select().from(runInputEvents)\n .where(and(eq(runInputEvents.runId, runId), gt(runInputEvents.sequence, afterSequence)))\n .orderBy(asc(runInputEvents.sequence));\n return rows.map(toInputEventRecord);\n }\n\n async markInputDelivered(runId: string, sequence: number): Promise<void> {\n await this.db.update(runInputEvents)\n .set({ deliveredAt: nowIso() })\n .where(and(eq(runInputEvents.runId, runId), eq(runInputEvents.sequence, sequence)));\n }\n}\n","import { loadServerConfig } from \"../../core/src/config.js\";\nimport { runMigration } from \"./migration.js\";\n\nasync function main(): Promise<void> {\n const config = loadServerConfig();\n await runMigration(config.databaseUrl);\n}\n\nmain().catch((error) => {\n process.stderr.write(`${String(error)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["artifacts","nodes","runInputEvents","runLogs","runs","tasks","isNull","sql","index","integer","text","uniqueIndex","mkdirSync","homedir","dirname","join","homedir","join","sql","and","asc","desc","eq","gt","dirname","Database","join","homedir","mkdirSync"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
CREATE TABLE "artifacts" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"run_id" text NOT NULL,
|
|
4
|
+
"type" text NOT NULL,
|
|
5
|
+
"name" text NOT NULL,
|
|
6
|
+
"path" text NOT NULL,
|
|
7
|
+
"metadata" jsonb,
|
|
8
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
9
|
+
);
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
CREATE TABLE "nodes" (
|
|
12
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
13
|
+
"name" text NOT NULL,
|
|
14
|
+
"capabilities" text[] DEFAULT '{}'::text[] NOT NULL,
|
|
15
|
+
"status" text DEFAULT 'online' NOT NULL,
|
|
16
|
+
"last_heartbeat_at" timestamp with time zone,
|
|
17
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
18
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
19
|
+
);
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
CREATE TABLE "run_input_events" (
|
|
22
|
+
"id" bigserial PRIMARY KEY NOT NULL,
|
|
23
|
+
"run_id" text NOT NULL,
|
|
24
|
+
"sequence" bigint NOT NULL,
|
|
25
|
+
"kind" text NOT NULL,
|
|
26
|
+
"payload" jsonb NOT NULL,
|
|
27
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
28
|
+
"delivered_at" timestamp with time zone
|
|
29
|
+
);
|
|
30
|
+
--> statement-breakpoint
|
|
31
|
+
CREATE TABLE "run_logs" (
|
|
32
|
+
"id" bigserial PRIMARY KEY NOT NULL,
|
|
33
|
+
"run_id" text NOT NULL,
|
|
34
|
+
"step_name" text,
|
|
35
|
+
"stream" text NOT NULL,
|
|
36
|
+
"message" text NOT NULL,
|
|
37
|
+
"timestamp" timestamp with time zone DEFAULT now() NOT NULL
|
|
38
|
+
);
|
|
39
|
+
--> statement-breakpoint
|
|
40
|
+
CREATE TABLE "runs" (
|
|
41
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
42
|
+
"task_id" text NOT NULL,
|
|
43
|
+
"node_id" text NOT NULL,
|
|
44
|
+
"attempt" integer NOT NULL,
|
|
45
|
+
"status" text NOT NULL,
|
|
46
|
+
"started_at" timestamp with time zone,
|
|
47
|
+
"ended_at" timestamp with time zone,
|
|
48
|
+
"error" text,
|
|
49
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
50
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
51
|
+
);
|
|
52
|
+
--> statement-breakpoint
|
|
53
|
+
CREATE TABLE "tasks" (
|
|
54
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
55
|
+
"kind" text NOT NULL,
|
|
56
|
+
"adapter" text NOT NULL,
|
|
57
|
+
"payload" jsonb NOT NULL,
|
|
58
|
+
"requires" text[] DEFAULT '{}'::text[] NOT NULL,
|
|
59
|
+
"status" text NOT NULL,
|
|
60
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
61
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
62
|
+
);
|
|
63
|
+
--> statement-breakpoint
|
|
64
|
+
ALTER TABLE "artifacts" ADD CONSTRAINT "artifacts_run_id_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
65
|
+
ALTER TABLE "run_input_events" ADD CONSTRAINT "run_input_events_run_id_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
66
|
+
ALTER TABLE "run_logs" ADD CONSTRAINT "run_logs_run_id_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
67
|
+
ALTER TABLE "runs" ADD CONSTRAINT "runs_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
68
|
+
ALTER TABLE "runs" ADD CONSTRAINT "runs_node_id_nodes_id_fk" FOREIGN KEY ("node_id") REFERENCES "public"."nodes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
69
|
+
CREATE INDEX "idx_artifacts_run_id" ON "artifacts" USING btree ("run_id");--> statement-breakpoint
|
|
70
|
+
CREATE UNIQUE INDEX "idx_run_input_events_run_sequence" ON "run_input_events" USING btree ("run_id","sequence");--> statement-breakpoint
|
|
71
|
+
CREATE INDEX "idx_run_input_events_undelivered" ON "run_input_events" USING btree ("run_id","sequence") WHERE "run_input_events"."delivered_at" is null;--> statement-breakpoint
|
|
72
|
+
CREATE INDEX "idx_run_logs_run_id_timestamp" ON "run_logs" USING btree ("run_id","timestamp");--> statement-breakpoint
|
|
73
|
+
CREATE INDEX "idx_runs_task_id" ON "runs" USING btree ("task_id");--> statement-breakpoint
|
|
74
|
+
CREATE INDEX "idx_runs_node_id" ON "runs" USING btree ("node_id");--> statement-breakpoint
|
|
75
|
+
CREATE INDEX "idx_tasks_status_created_at" ON "tasks" USING btree ("status","created_at");
|