workplane 0.4.1 → 0.4.3

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/setup.js CHANGED
@@ -17,11 +17,14 @@ __export(pg_exports, {
17
17
  runInputEvents: () => runInputEvents,
18
18
  runLogs: () => runLogs,
19
19
  runs: () => runs,
20
- tasks: () => tasks
20
+ tasks: () => tasks,
21
+ workplanRuns: () => workplanRuns,
22
+ workplanSchedules: () => workplanSchedules,
23
+ workplanStepResults: () => workplanStepResults
21
24
  });
22
25
  import { isNull, sql } from "drizzle-orm";
23
- import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
24
- var tasks, nodes, runs, runLogs, artifacts, runInputEvents;
26
+ import { bigint, bigserial, boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
27
+ var tasks, nodes, runs, runLogs, artifacts, workplanSchedules, workplanRuns, workplanStepResults, runInputEvents;
25
28
  var init_pg = __esm({
26
29
  "../db/src/schema/pg.ts"() {
27
30
  "use strict";
@@ -82,6 +85,49 @@ var init_pg = __esm({
82
85
  }, (table) => ({
83
86
  runIdIdx: index("idx_artifacts_run_id").on(table.runId)
84
87
  }));
88
+ workplanSchedules = pgTable("workplan_schedules", {
89
+ id: text("id").primaryKey(),
90
+ planId: text("plan_id").notNull(),
91
+ name: text("name").notNull(),
92
+ cronExpression: text("cron_expression").notNull(),
93
+ timezone: text("timezone").notNull(),
94
+ inputs: jsonb("inputs").$type().notNull().default({}),
95
+ enabled: boolean("enabled").notNull().default(true),
96
+ lastRunAt: timestamp("last_run_at", { withTimezone: true }),
97
+ nextRunAt: timestamp("next_run_at", { withTimezone: true }),
98
+ createdBy: text("created_by"),
99
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
100
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
101
+ }, (table) => ({
102
+ dueIdx: index("idx_workplan_schedules_due").on(table.nextRunAt)
103
+ }));
104
+ workplanRuns = pgTable("workplan_runs", {
105
+ id: text("id").primaryKey(),
106
+ scheduleId: text("schedule_id").references(() => workplanSchedules.id),
107
+ planId: text("plan_id").notNull(),
108
+ planName: text("plan_name").notNull(),
109
+ status: text("status").notNull().default("running"),
110
+ idempotencyKey: text("idempotency_key"),
111
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
112
+ endedAt: timestamp("ended_at", { withTimezone: true }),
113
+ error: text("error")
114
+ }, (table) => ({
115
+ scheduleIdx: index("idx_workplan_runs_schedule_id").on(table.scheduleId),
116
+ idempotencyUnique: uniqueIndex("idx_workplan_runs_idempotency").on(table.idempotencyKey)
117
+ }));
118
+ workplanStepResults = pgTable("workplan_step_results", {
119
+ id: text("id").primaryKey(),
120
+ workplanRunId: text("workplan_run_id").notNull().references(() => workplanRuns.id),
121
+ stepId: text("step_id").notNull(),
122
+ stepName: text("step_name").notNull(),
123
+ output: text("output"),
124
+ exitCode: integer("exit_code"),
125
+ durationMs: integer("duration_ms"),
126
+ metadata: jsonb("metadata").$type(),
127
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
128
+ }, (table) => ({
129
+ runIdx: index("idx_workplan_step_results_run_id").on(table.workplanRunId)
130
+ }));
85
131
  runInputEvents = pgTable("run_input_events", {
86
132
  id: bigserial("id", { mode: "number" }).primaryKey(),
87
133
  runId: text("run_id").notNull().references(() => runs.id),
@@ -98,7 +144,7 @@ var init_pg = __esm({
98
144
  });
99
145
 
100
146
  // ../db/src/pg-store.ts
101
- import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
147
+ import { and, arrayContains, asc, desc, eq, gt, isNotNull, lte, sql as sql2 } from "drizzle-orm";
102
148
  var init_pg_store = __esm({
103
149
  "../db/src/pg-store.ts"() {
104
150
  "use strict";
@@ -114,11 +160,14 @@ __export(sqlite_exports, {
114
160
  runInputEvents: () => runInputEvents2,
115
161
  runLogs: () => runLogs2,
116
162
  runs: () => runs2,
117
- tasks: () => tasks2
163
+ tasks: () => tasks2,
164
+ workplanRuns: () => workplanRuns2,
165
+ workplanSchedules: () => workplanSchedules2,
166
+ workplanStepResults: () => workplanStepResults2
118
167
  });
119
168
  import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
120
169
  import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
121
- var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, runInputEvents2;
170
+ var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, workplanSchedules2, workplanRuns2, workplanStepResults2, runInputEvents2;
122
171
  var init_sqlite = __esm({
123
172
  "../db/src/schema/sqlite.ts"() {
124
173
  "use strict";
@@ -180,6 +229,49 @@ var init_sqlite = __esm({
180
229
  }, (table) => ({
181
230
  runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
182
231
  }));
232
+ workplanSchedules2 = sqliteTable("workplan_schedules", {
233
+ id: text2("id").primaryKey(),
234
+ planId: text2("plan_id").notNull(),
235
+ name: text2("name").notNull(),
236
+ cronExpression: text2("cron_expression").notNull(),
237
+ timezone: text2("timezone").notNull(),
238
+ inputs: text2("inputs", { mode: "json" }).$type().notNull().default({}),
239
+ enabled: integer2("enabled", { mode: "boolean" }).notNull().default(true),
240
+ lastRunAt: text2("last_run_at"),
241
+ nextRunAt: text2("next_run_at"),
242
+ createdBy: text2("created_by"),
243
+ createdAt: text2("created_at").notNull().default(nowDefault),
244
+ updatedAt: text2("updated_at").notNull().default(nowDefault)
245
+ }, (table) => ({
246
+ dueIdx: index2("idx_workplan_schedules_due").on(table.nextRunAt)
247
+ }));
248
+ workplanRuns2 = sqliteTable("workplan_runs", {
249
+ id: text2("id").primaryKey(),
250
+ scheduleId: text2("schedule_id").references(() => workplanSchedules2.id),
251
+ planId: text2("plan_id").notNull(),
252
+ planName: text2("plan_name").notNull(),
253
+ status: text2("status").notNull().default("running"),
254
+ idempotencyKey: text2("idempotency_key"),
255
+ createdAt: text2("created_at").notNull().default(nowDefault),
256
+ endedAt: text2("ended_at"),
257
+ error: text2("error")
258
+ }, (table) => ({
259
+ scheduleIdx: index2("idx_workplan_runs_schedule_id").on(table.scheduleId),
260
+ idempotencyUnique: uniqueIndex2("idx_workplan_runs_idempotency").on(table.idempotencyKey)
261
+ }));
262
+ workplanStepResults2 = sqliteTable("workplan_step_results", {
263
+ id: text2("id").primaryKey(),
264
+ workplanRunId: text2("workplan_run_id").notNull().references(() => workplanRuns2.id),
265
+ stepId: text2("step_id").notNull(),
266
+ stepName: text2("step_name").notNull(),
267
+ output: text2("output"),
268
+ exitCode: integer2("exit_code"),
269
+ durationMs: integer2("duration_ms"),
270
+ metadata: text2("metadata", { mode: "json" }).$type(),
271
+ createdAt: text2("created_at").notNull().default(nowDefault)
272
+ }, (table) => ({
273
+ runIdx: index2("idx_workplan_step_results_run_id").on(table.workplanRunId)
274
+ }));
183
275
  runInputEvents2 = sqliteTable("run_input_events", {
184
276
  id: integer2("id").primaryKey({ autoIncrement: true }),
185
277
  runId: text2("run_id").notNull().references(() => runs2.id),
@@ -196,7 +288,7 @@ var init_sqlite = __esm({
196
288
  });
197
289
 
198
290
  // ../db/src/sqlite-store.ts
199
- import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
291
+ import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2, lte as lte2, sql as sql4 } from "drizzle-orm";
200
292
  var init_sqlite_store = __esm({
201
293
  "../db/src/sqlite-store.ts"() {
202
294
  "use strict";
@@ -234,6 +326,149 @@ var init_client = __esm({
234
326
  }
235
327
  });
236
328
 
329
+ // ../db/src/baseline-migrations.ts
330
+ import { createHash } from "crypto";
331
+ import { readFileSync } from "fs";
332
+ import { resolve } from "path";
333
+ function readJournal(migrationsFolder) {
334
+ return JSON.parse(
335
+ readFileSync(resolve(migrationsFolder, "meta/_journal.json"), "utf8")
336
+ );
337
+ }
338
+ function migrationHash(migrationsFolder, tag) {
339
+ const sql5 = readFileSync(resolve(migrationsFolder, `${tag}.sql`), "utf8");
340
+ return createHash("sha256").update(sql5).digest("hex");
341
+ }
342
+ function baselineEntries(migrationsFolder, upToIdx) {
343
+ const journal = readJournal(migrationsFolder);
344
+ return journal.entries.filter((entry) => entry.idx <= upToIdx).map((entry) => ({
345
+ hash: migrationHash(migrationsFolder, entry.tag),
346
+ createdAt: entry.when,
347
+ tag: entry.tag
348
+ }));
349
+ }
350
+ async function tableExistsPg(pool, tableName) {
351
+ const result = await pool.query(
352
+ `SELECT EXISTS (
353
+ SELECT 1 FROM information_schema.tables
354
+ WHERE table_schema = 'public' AND table_name = $1
355
+ ) AS exists`,
356
+ [tableName]
357
+ );
358
+ return Boolean(result.rows[0]?.exists);
359
+ }
360
+ async function getLastMigrationCreatedAtPg(pool) {
361
+ await pool.query("CREATE SCHEMA IF NOT EXISTS drizzle");
362
+ await pool.query(`
363
+ CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
364
+ id SERIAL PRIMARY KEY,
365
+ hash text NOT NULL,
366
+ created_at bigint
367
+ )
368
+ `);
369
+ const result = await pool.query(
370
+ "SELECT created_at FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 1"
371
+ );
372
+ const value = result.rows[0]?.created_at;
373
+ return value === null || value === void 0 ? null : Number(value);
374
+ }
375
+ async function insertBaselinePg(pool, entries) {
376
+ const inserted = [];
377
+ const lastCreatedAt = await getLastMigrationCreatedAtPg(pool);
378
+ for (const entry of entries) {
379
+ if (lastCreatedAt !== null && lastCreatedAt >= entry.createdAt) {
380
+ continue;
381
+ }
382
+ const existing = await pool.query(
383
+ "SELECT id FROM drizzle.__drizzle_migrations WHERE created_at = $1 LIMIT 1",
384
+ [entry.createdAt]
385
+ );
386
+ if (existing.rowCount) {
387
+ continue;
388
+ }
389
+ await pool.query(
390
+ 'INSERT INTO drizzle.__drizzle_migrations ("hash", "created_at") VALUES ($1, $2)',
391
+ [entry.hash, entry.createdAt]
392
+ );
393
+ inserted.push(entry.tag);
394
+ }
395
+ return inserted;
396
+ }
397
+ async function baselineLegacyPostgresMigrations(pool, migrationsFolder) {
398
+ const hasTasks = await tableExistsPg(pool, "tasks");
399
+ if (!hasTasks) {
400
+ return;
401
+ }
402
+ const hasWorkplanSchedules = await tableExistsPg(pool, "workplan_schedules");
403
+ const upToIdx = hasWorkplanSchedules ? 1 : 0;
404
+ const inserted = await insertBaselinePg(pool, baselineEntries(migrationsFolder, upToIdx));
405
+ if (inserted.length > 0) {
406
+ process.stdout.write(
407
+ `Baselined existing Postgres schema for migration(s): ${inserted.join(", ")}
408
+ `
409
+ );
410
+ }
411
+ }
412
+ function tableExistsSqlite(db, tableName) {
413
+ const row = db.prepare(
414
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1"
415
+ ).get(tableName);
416
+ return Boolean(row);
417
+ }
418
+ function getLastMigrationCreatedAtSqlite(db) {
419
+ db.exec(`
420
+ CREATE TABLE IF NOT EXISTS __drizzle_migrations (
421
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
422
+ hash text NOT NULL,
423
+ created_at numeric
424
+ )
425
+ `);
426
+ const row = db.prepare(
427
+ "SELECT created_at FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 1"
428
+ ).get();
429
+ return row?.created_at ?? null;
430
+ }
431
+ function insertBaselineSqlite(db, entries) {
432
+ const inserted = [];
433
+ const lastCreatedAt = getLastMigrationCreatedAtSqlite(db);
434
+ const insert = db.prepare(
435
+ 'INSERT INTO __drizzle_migrations ("hash", "created_at") VALUES (?, ?)'
436
+ );
437
+ const exists = db.prepare(
438
+ "SELECT 1 FROM __drizzle_migrations WHERE created_at = ? LIMIT 1"
439
+ );
440
+ for (const entry of entries) {
441
+ if (lastCreatedAt !== null && lastCreatedAt >= entry.createdAt) {
442
+ continue;
443
+ }
444
+ if (exists.get(entry.createdAt)) {
445
+ continue;
446
+ }
447
+ insert.run(entry.hash, entry.createdAt);
448
+ inserted.push(entry.tag);
449
+ }
450
+ return inserted;
451
+ }
452
+ function baselineLegacySqliteMigrations(db, migrationsFolder) {
453
+ if (!tableExistsSqlite(db, "tasks")) {
454
+ return;
455
+ }
456
+ const hasWorkplanSchedules = tableExistsSqlite(db, "workplan_schedules");
457
+ const upToIdx = hasWorkplanSchedules ? 1 : 0;
458
+ const inserted = insertBaselineSqlite(db, baselineEntries(migrationsFolder, upToIdx));
459
+ if (inserted.length > 0) {
460
+ process.stdout.write(
461
+ `Baselined existing SQLite schema for migration(s): ${inserted.join(", ")}
462
+ `
463
+ );
464
+ }
465
+ }
466
+ var init_baseline_migrations = __esm({
467
+ "../db/src/baseline-migrations.ts"() {
468
+ "use strict";
469
+ }
470
+ });
471
+
237
472
  // ../db/src/migration.ts
238
473
  var migration_exports = {};
239
474
  __export(migration_exports, {
@@ -241,7 +476,7 @@ __export(migration_exports, {
241
476
  });
242
477
  import { mkdirSync as mkdirSync2 } from "fs";
243
478
  import { homedir as homedir2 } from "os";
244
- import { dirname as dirname2, join as join2, resolve } from "path";
479
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
245
480
  import { fileURLToPath } from "url";
246
481
  async function migratePostgres(databaseUrl) {
247
482
  const adminUrl = new URL(databaseUrl);
@@ -271,8 +506,10 @@ async function migratePostgres(databaseUrl) {
271
506
  await adminPool.end();
272
507
  }
273
508
  const pool = getPool(databaseUrl);
509
+ const migrationsFolder = resolve2(distDir, "migrations/pg");
510
+ await baselineLegacyPostgresMigrations(pool, migrationsFolder);
274
511
  const { migrate } = await import("drizzle-orm/node-postgres/migrator");
275
- await migrate(getDrizzle(pool), { migrationsFolder: resolve(distDir, "migrations/pg") });
512
+ await migrate(getDrizzle(pool), { migrationsFolder });
276
513
  await pool.end();
277
514
  process.stdout.write("Postgres migrations applied.\n");
278
515
  }
@@ -287,8 +524,10 @@ async function migrateSqlite(databaseUrl) {
287
524
  const sqlite = new Database2(filePath);
288
525
  sqlite.pragma("journal_mode = WAL");
289
526
  sqlite.pragma("synchronous = NORMAL");
527
+ const migrationsFolder = resolve2(distDir, "migrations/sqlite");
528
+ baselineLegacySqliteMigrations(sqlite, migrationsFolder);
290
529
  const db = drizzle(sqlite, { schema: sqliteSchema });
291
- migrate(db, { migrationsFolder: resolve(distDir, "migrations/sqlite") });
530
+ migrate(db, { migrationsFolder });
292
531
  sqlite.close();
293
532
  process.stdout.write(`SQLite migrations applied: ${filePath}
294
533
  `);
@@ -305,13 +544,14 @@ var init_migration = __esm({
305
544
  "../db/src/migration.ts"() {
306
545
  "use strict";
307
546
  init_client();
547
+ init_baseline_migrations();
308
548
  distDir = dirname2(fileURLToPath(import.meta.url));
309
549
  }
310
550
  });
311
551
 
312
552
  // ../cli/src/setup.ts
313
553
  import { randomBytes } from "crypto";
314
- import { existsSync, mkdirSync as mkdirSync3, readFileSync, writeFileSync } from "fs";
554
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
315
555
  import { homedir as homedir3 } from "os";
316
556
  import { join as join3 } from "path";
317
557
  import { createInterface } from "readline/promises";
@@ -323,7 +563,7 @@ function generateToken() {
323
563
  function parseEnvFile(path) {
324
564
  try {
325
565
  const result = {};
326
- for (const line of readFileSync(path, "utf8").split("\n")) {
566
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
327
567
  const t = line.trim();
328
568
  if (!t || t.startsWith("#")) continue;
329
569
  const eq3 = t.indexOf("=");