opencode-swarm-plugin 0.12.31 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.beads/issues.jsonl +204 -10
  2. package/.opencode/skills/tdd/SKILL.md +182 -0
  3. package/README.md +165 -17
  4. package/bun.lock +23 -0
  5. package/dist/index.js +4020 -438
  6. package/dist/pglite.data +0 -0
  7. package/dist/pglite.wasm +0 -0
  8. package/dist/plugin.js +4008 -514
  9. package/examples/commands/swarm.md +51 -7
  10. package/examples/skills/beads-workflow/SKILL.md +75 -28
  11. package/examples/skills/swarm-coordination/SKILL.md +92 -1
  12. package/global-skills/testing-patterns/SKILL.md +430 -0
  13. package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +586 -0
  14. package/package.json +11 -5
  15. package/src/index.ts +44 -5
  16. package/src/streams/agent-mail.test.ts +777 -0
  17. package/src/streams/agent-mail.ts +535 -0
  18. package/src/streams/debug.test.ts +500 -0
  19. package/src/streams/debug.ts +629 -0
  20. package/src/streams/effect/ask.integration.test.ts +314 -0
  21. package/src/streams/effect/ask.ts +202 -0
  22. package/src/streams/effect/cursor.integration.test.ts +418 -0
  23. package/src/streams/effect/cursor.ts +288 -0
  24. package/src/streams/effect/deferred.test.ts +357 -0
  25. package/src/streams/effect/deferred.ts +445 -0
  26. package/src/streams/effect/index.ts +17 -0
  27. package/src/streams/effect/layers.ts +73 -0
  28. package/src/streams/effect/lock.test.ts +385 -0
  29. package/src/streams/effect/lock.ts +399 -0
  30. package/src/streams/effect/mailbox.test.ts +260 -0
  31. package/src/streams/effect/mailbox.ts +318 -0
  32. package/src/streams/events.test.ts +628 -0
  33. package/src/streams/events.ts +214 -0
  34. package/src/streams/index.test.ts +229 -0
  35. package/src/streams/index.ts +492 -0
  36. package/src/streams/migrations.test.ts +355 -0
  37. package/src/streams/migrations.ts +269 -0
  38. package/src/streams/projections.test.ts +611 -0
  39. package/src/streams/projections.ts +302 -0
  40. package/src/streams/store.integration.test.ts +548 -0
  41. package/src/streams/store.ts +546 -0
  42. package/src/streams/swarm-mail.ts +552 -0
  43. package/src/swarm-mail.integration.test.ts +970 -0
  44. package/src/swarm-mail.ts +739 -0
  45. package/src/swarm.ts +84 -59
  46. package/src/tool-availability.ts +35 -2
  47. package/global-skills/mcp-tool-authoring/SKILL.md +0 -695
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Tests for Schema Migration System
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import { PGlite } from "@electric-sql/pglite";
6
+ import {
7
+ runMigrations,
8
+ getCurrentVersion,
9
+ rollbackTo,
10
+ isMigrationApplied,
11
+ getPendingMigrations,
12
+ getAppliedMigrations,
13
+ migrations,
14
+ } from "./migrations";
15
+
16
+ describe("Schema Migrations", () => {
17
+ let db: PGlite;
18
+
19
+ beforeEach(async () => {
20
+ // Use in-memory database for tests
21
+ db = new PGlite();
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await db.close();
26
+ });
27
+
28
+ describe("Fresh Install", () => {
29
+ it("should start with version 0", async () => {
30
+ const version = await getCurrentVersion(db);
31
+ expect(version).toBe(0);
32
+ });
33
+
34
+ it("should run all migrations on fresh database", async () => {
35
+ const result = await runMigrations(db);
36
+
37
+ expect(result.applied).toEqual([1, 2]);
38
+ expect(result.current).toBe(2);
39
+
40
+ const version = await getCurrentVersion(db);
41
+ expect(version).toBe(2);
42
+ });
43
+
44
+ it("should create cursors table with correct schema", async () => {
45
+ await runMigrations(db);
46
+
47
+ // Verify table exists
48
+ const tableResult = await db.query<{ exists: boolean }>(
49
+ `SELECT EXISTS (
50
+ SELECT FROM information_schema.tables
51
+ WHERE table_name = 'cursors'
52
+ ) as exists`,
53
+ );
54
+ expect(tableResult.rows[0]?.exists).toBe(true);
55
+
56
+ // Verify columns
57
+ const columnsResult = await db.query<{ column_name: string }>(
58
+ `SELECT column_name FROM information_schema.columns
59
+ WHERE table_name = 'cursors'
60
+ ORDER BY ordinal_position`,
61
+ );
62
+ const columns = columnsResult.rows.map((r) => r.column_name);
63
+ expect(columns).toContain("id");
64
+ expect(columns).toContain("stream");
65
+ expect(columns).toContain("checkpoint");
66
+ expect(columns).toContain("position");
67
+ expect(columns).toContain("updated_at");
68
+
69
+ // Verify unique constraint exists
70
+ const constraintsResult = await db.query<{ constraint_name: string }>(
71
+ `SELECT constraint_name FROM information_schema.table_constraints
72
+ WHERE table_name = 'cursors' AND constraint_type = 'UNIQUE'`,
73
+ );
74
+ expect(constraintsResult.rows.length).toBeGreaterThan(0);
75
+ });
76
+
77
+ it("should create deferred table with correct schema", async () => {
78
+ await runMigrations(db);
79
+
80
+ const tableResult = await db.query<{ exists: boolean }>(
81
+ `SELECT EXISTS (
82
+ SELECT FROM information_schema.tables
83
+ WHERE table_name = 'deferred'
84
+ ) as exists`,
85
+ );
86
+ expect(tableResult.rows[0]?.exists).toBe(true);
87
+
88
+ const columnsResult = await db.query<{ column_name: string }>(
89
+ `SELECT column_name FROM information_schema.columns
90
+ WHERE table_name = 'deferred'
91
+ ORDER BY ordinal_position`,
92
+ );
93
+ const columns = columnsResult.rows.map((r) => r.column_name);
94
+ expect(columns).toContain("id");
95
+ expect(columns).toContain("url");
96
+ expect(columns).toContain("resolved");
97
+ expect(columns).toContain("value");
98
+ expect(columns).toContain("error");
99
+ expect(columns).toContain("expires_at");
100
+ expect(columns).toContain("created_at");
101
+ });
102
+ });
103
+
104
+ describe("Idempotency", () => {
105
+ it("should be safe to run migrations multiple times", async () => {
106
+ // First run
107
+ const result1 = await runMigrations(db);
108
+ expect(result1.applied).toEqual([1, 2]);
109
+
110
+ // Second run - should apply nothing
111
+ const result2 = await runMigrations(db);
112
+ expect(result2.applied).toEqual([]);
113
+ expect(result2.current).toBe(2);
114
+
115
+ // Version should still be 2
116
+ const version = await getCurrentVersion(db);
117
+ expect(version).toBe(2);
118
+ });
119
+ });
120
+
121
+ describe("Incremental Upgrade", () => {
122
+ it("should apply only new migrations", async () => {
123
+ // Manually apply migration 1
124
+ await db.exec(migrations[0]!.up);
125
+ await db.exec(`
126
+ CREATE TABLE IF NOT EXISTS schema_version (
127
+ version INTEGER PRIMARY KEY,
128
+ applied_at BIGINT NOT NULL,
129
+ description TEXT
130
+ );
131
+ `);
132
+ await db.query(
133
+ `INSERT INTO schema_version (version, applied_at, description)
134
+ VALUES ($1, $2, $3)`,
135
+ [1, Date.now(), migrations[0]!.description],
136
+ );
137
+
138
+ // Now run migrations - should only apply 2
139
+ const result = await runMigrations(db);
140
+ expect(result.applied).toEqual([2]);
141
+ expect(result.current).toBe(2);
142
+ });
143
+ });
144
+
145
+ describe("Rollback", () => {
146
+ it("should rollback to target version", async () => {
147
+ // Apply all migrations
148
+ await runMigrations(db);
149
+ expect(await getCurrentVersion(db)).toBe(2);
150
+
151
+ // Rollback to version 1
152
+ const result = await rollbackTo(db, 1);
153
+ expect(result.rolledBack).toEqual([2]);
154
+ expect(result.current).toBe(1);
155
+
156
+ // Version should be 1
157
+ const version = await getCurrentVersion(db);
158
+ expect(version).toBe(1);
159
+
160
+ // Cursors table should still exist
161
+ const cursorsExists = await db.query<{ exists: boolean }>(
162
+ `SELECT EXISTS (
163
+ SELECT FROM information_schema.tables
164
+ WHERE table_name = 'cursors'
165
+ ) as exists`,
166
+ );
167
+ expect(cursorsExists.rows[0]?.exists).toBe(true);
168
+
169
+ // Deferred table should be gone
170
+ const deferredExists = await db.query<{ exists: boolean }>(
171
+ `SELECT EXISTS (
172
+ SELECT FROM information_schema.tables
173
+ WHERE table_name = 'deferred'
174
+ ) as exists`,
175
+ );
176
+ expect(deferredExists.rows[0]?.exists).toBe(false);
177
+ });
178
+
179
+ it("should rollback to version 0", async () => {
180
+ await runMigrations(db);
181
+
182
+ const result = await rollbackTo(db, 0);
183
+ expect(result.rolledBack).toEqual([2, 1]);
184
+ expect(result.current).toBe(0);
185
+
186
+ // All tables should be gone
187
+ const cursorsExists = await db.query<{ exists: boolean }>(
188
+ `SELECT EXISTS (
189
+ SELECT FROM information_schema.tables
190
+ WHERE table_name = 'cursors'
191
+ ) as exists`,
192
+ );
193
+ expect(cursorsExists.rows[0]?.exists).toBe(false);
194
+ });
195
+
196
+ it("should do nothing if target version >= current", async () => {
197
+ await runMigrations(db);
198
+
199
+ const result = await rollbackTo(db, 2);
200
+ expect(result.rolledBack).toEqual([]);
201
+ expect(result.current).toBe(2);
202
+ });
203
+ });
204
+
205
+ describe("Migration Status", () => {
206
+ it("should check if migration is applied", async () => {
207
+ expect(await isMigrationApplied(db, 1)).toBe(false);
208
+
209
+ await runMigrations(db);
210
+
211
+ expect(await isMigrationApplied(db, 1)).toBe(true);
212
+ expect(await isMigrationApplied(db, 2)).toBe(true);
213
+ });
214
+
215
+ it("should list pending migrations", async () => {
216
+ const pending1 = await getPendingMigrations(db);
217
+ expect(pending1).toHaveLength(2);
218
+ expect(pending1.map((m) => m.version)).toEqual([1, 2]);
219
+
220
+ // Apply migration 1
221
+ const migration = migrations[0];
222
+ if (!migration) throw new Error("Migration not found");
223
+
224
+ await db.exec(migration.up);
225
+ await db.exec(`
226
+ CREATE TABLE IF NOT EXISTS schema_version (
227
+ version INTEGER PRIMARY KEY,
228
+ applied_at BIGINT NOT NULL,
229
+ description TEXT
230
+ );
231
+ `);
232
+ await db.query(
233
+ `INSERT INTO schema_version (version, applied_at, description)
234
+ VALUES ($1, $2, $3)`,
235
+ [1, Date.now(), migration.description],
236
+ );
237
+
238
+ const pending2 = await getPendingMigrations(db);
239
+ expect(pending2).toHaveLength(1);
240
+ expect(pending2.map((m) => m.version)).toEqual([2]);
241
+ });
242
+
243
+ it("should list applied migrations", async () => {
244
+ const applied1 = await getAppliedMigrations(db);
245
+ expect(applied1).toHaveLength(0);
246
+
247
+ await runMigrations(db);
248
+
249
+ const applied2 = await getAppliedMigrations(db);
250
+ expect(applied2).toHaveLength(2);
251
+ expect(applied2.map((m) => m.version)).toEqual([1, 2]);
252
+ expect(applied2[0]?.description).toBe(
253
+ "Add cursors table for DurableCursor",
254
+ );
255
+ });
256
+ });
257
+
258
+ describe("Data Persistence", () => {
259
+ it("should preserve data across migrations", async () => {
260
+ // Apply migration 1 (cursors table)
261
+ await db.exec(migrations[0]!.up);
262
+ await db.exec(`
263
+ CREATE TABLE IF NOT EXISTS schema_version (
264
+ version INTEGER PRIMARY KEY,
265
+ applied_at BIGINT NOT NULL,
266
+ description TEXT
267
+ );
268
+ `);
269
+ await db.query(
270
+ `INSERT INTO schema_version (version, applied_at, description)
271
+ VALUES ($1, $2, $3)`,
272
+ [1, Date.now(), migrations[0]!.description],
273
+ );
274
+
275
+ // Insert test data
276
+ await db.query(
277
+ `INSERT INTO cursors (stream, checkpoint, position, updated_at)
278
+ VALUES ($1, $2, $3, $4)`,
279
+ ["test-stream", "test-checkpoint", 42, Date.now()],
280
+ );
281
+
282
+ // Apply remaining migrations
283
+ await runMigrations(db);
284
+
285
+ // Data should still be there
286
+ const result = await db.query<{ position: number }>(
287
+ `SELECT position FROM cursors WHERE stream = $1`,
288
+ ["test-stream"],
289
+ );
290
+ expect(result.rows[0]?.position).toBe(42);
291
+ });
292
+ });
293
+
294
+ describe("Error Handling", () => {
295
+ it("should rollback failed migrations", async () => {
296
+ // Apply good migration first
297
+ const migration = migrations[0];
298
+ if (!migration) throw new Error("Migration not found");
299
+
300
+ await db.exec(migration.up);
301
+ await db.exec(`
302
+ CREATE TABLE IF NOT EXISTS schema_version (
303
+ version INTEGER PRIMARY KEY,
304
+ applied_at BIGINT NOT NULL,
305
+ description TEXT
306
+ );
307
+ `);
308
+ await db.query(
309
+ `INSERT INTO schema_version (version, applied_at, description)
310
+ VALUES ($1, $2, $3)`,
311
+ [1, Date.now(), migration.description],
312
+ );
313
+
314
+ // Try to run invalid SQL in a transaction
315
+ try {
316
+ await db.exec("BEGIN");
317
+ await db.exec("THIS IS INVALID SQL");
318
+ await db.exec("COMMIT");
319
+ throw new Error("Should have thrown");
320
+ } catch {
321
+ await db.exec("ROLLBACK");
322
+ // Expected to fail
323
+ }
324
+
325
+ // Version should still be 1
326
+ const version = await getCurrentVersion(db);
327
+ expect(version).toBe(1);
328
+ });
329
+ });
330
+
331
+ describe("Schema Version Table", () => {
332
+ it("should record migration metadata", async () => {
333
+ await runMigrations(db);
334
+
335
+ const result = await db.query<{
336
+ version: number;
337
+ applied_at: string;
338
+ description: string;
339
+ }>(
340
+ `SELECT version, applied_at, description FROM schema_version ORDER BY version`,
341
+ );
342
+
343
+ expect(result.rows).toHaveLength(2);
344
+ expect(result.rows[0]?.version).toBe(1);
345
+ expect(result.rows[0]?.description).toBe(
346
+ "Add cursors table for DurableCursor",
347
+ );
348
+ expect(result.rows[1]?.version).toBe(2);
349
+
350
+ // Applied_at should be recent
351
+ const appliedAt = parseInt(result.rows[0]?.applied_at as string);
352
+ expect(appliedAt).toBeGreaterThan(Date.now() - 10000);
353
+ });
354
+ });
355
+ });
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Schema Migration System for PGLite Event Store
3
+ *
4
+ * Version-based migrations with up/down support.
5
+ * Tracks applied migrations in schema_version table.
6
+ * Idempotent - safe to run multiple times.
7
+ */
8
+ import type { PGlite } from "@electric-sql/pglite";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface Migration {
15
+ version: number;
16
+ description: string;
17
+ up: string;
18
+ down: string;
19
+ }
20
+
21
+ interface SchemaVersion {
22
+ version: number;
23
+ applied_at: number;
24
+ description: string | null;
25
+ }
26
+
27
+ // ============================================================================
28
+ // Migration Definitions
29
+ // ============================================================================
30
+
31
+ export const migrations: Migration[] = [
32
+ {
33
+ version: 1,
34
+ description: "Add cursors table for DurableCursor",
35
+ up: `
36
+ CREATE TABLE IF NOT EXISTS cursors (
37
+ id SERIAL PRIMARY KEY,
38
+ stream TEXT NOT NULL,
39
+ checkpoint TEXT NOT NULL,
40
+ position BIGINT NOT NULL DEFAULT 0,
41
+ updated_at BIGINT NOT NULL,
42
+ UNIQUE(stream, checkpoint)
43
+ );
44
+ CREATE INDEX IF NOT EXISTS idx_cursors_checkpoint ON cursors(checkpoint);
45
+ CREATE INDEX IF NOT EXISTS idx_cursors_stream ON cursors(stream);
46
+ `,
47
+ down: `DROP TABLE IF EXISTS cursors;`,
48
+ },
49
+ {
50
+ version: 2,
51
+ description: "Add deferred table for DurableDeferred",
52
+ up: `
53
+ CREATE TABLE IF NOT EXISTS deferred (
54
+ id SERIAL PRIMARY KEY,
55
+ url TEXT NOT NULL UNIQUE,
56
+ resolved BOOLEAN NOT NULL DEFAULT FALSE,
57
+ value JSONB,
58
+ error TEXT,
59
+ expires_at BIGINT NOT NULL,
60
+ created_at BIGINT NOT NULL
61
+ );
62
+ CREATE INDEX IF NOT EXISTS idx_deferred_url ON deferred(url);
63
+ CREATE INDEX IF NOT EXISTS idx_deferred_expires ON deferred(expires_at);
64
+ CREATE INDEX IF NOT EXISTS idx_deferred_resolved ON deferred(resolved);
65
+ `,
66
+ down: `DROP TABLE IF EXISTS deferred;`,
67
+ },
68
+ ];
69
+
70
+ // ============================================================================
71
+ // Migration Execution
72
+ // ============================================================================
73
+
74
+ /**
75
+ * Initialize schema_version table if it doesn't exist
76
+ */
77
+ async function ensureVersionTable(db: PGlite): Promise<void> {
78
+ await db.exec(`
79
+ CREATE TABLE IF NOT EXISTS schema_version (
80
+ version INTEGER PRIMARY KEY,
81
+ applied_at BIGINT NOT NULL,
82
+ description TEXT
83
+ );
84
+ `);
85
+ }
86
+
87
+ /**
88
+ * Get the current schema version
89
+ *
90
+ * Returns 0 if no migrations have been applied
91
+ */
92
+ export async function getCurrentVersion(db: PGlite): Promise<number> {
93
+ await ensureVersionTable(db);
94
+
95
+ const result = await db.query<{ version: number }>(
96
+ `SELECT MAX(version) as version FROM schema_version`,
97
+ );
98
+
99
+ return result.rows[0]?.version ?? 0;
100
+ }
101
+
102
+ /**
103
+ * Get all applied migrations
104
+ */
105
+ export async function getAppliedMigrations(
106
+ db: PGlite,
107
+ ): Promise<SchemaVersion[]> {
108
+ await ensureVersionTable(db);
109
+
110
+ const result = await db.query<{
111
+ version: number;
112
+ applied_at: string;
113
+ description: string | null;
114
+ }>(
115
+ `SELECT version, applied_at, description FROM schema_version ORDER BY version ASC`,
116
+ );
117
+
118
+ return result.rows.map((row) => ({
119
+ version: row.version,
120
+ applied_at: parseInt(row.applied_at as string),
121
+ description: row.description,
122
+ }));
123
+ }
124
+
125
+ /**
126
+ * Run all pending migrations
127
+ *
128
+ * Idempotent - safe to run multiple times.
129
+ * Only runs migrations that haven't been applied yet.
130
+ */
131
+ export async function runMigrations(db: PGlite): Promise<{
132
+ applied: number[];
133
+ current: number;
134
+ }> {
135
+ await ensureVersionTable(db);
136
+
137
+ const currentVersion = await getCurrentVersion(db);
138
+ const applied: number[] = [];
139
+
140
+ // Find migrations that need to be applied
141
+ const pendingMigrations = migrations.filter(
142
+ (m) => m.version > currentVersion,
143
+ );
144
+
145
+ if (pendingMigrations.length === 0) {
146
+ return { applied: [], current: currentVersion };
147
+ }
148
+
149
+ // Sort by version to ensure correct order
150
+ pendingMigrations.sort((a, b) => a.version - b.version);
151
+
152
+ // Apply each migration in a transaction
153
+ for (const migration of pendingMigrations) {
154
+ await db.exec("BEGIN");
155
+ try {
156
+ // Run the migration SQL
157
+ await db.exec(migration.up);
158
+
159
+ // Record the migration
160
+ await db.query(
161
+ `INSERT INTO schema_version (version, applied_at, description)
162
+ VALUES ($1, $2, $3)`,
163
+ [migration.version, Date.now(), migration.description],
164
+ );
165
+
166
+ await db.exec("COMMIT");
167
+ applied.push(migration.version);
168
+
169
+ console.log(
170
+ `[migrations] Applied migration ${migration.version}: ${migration.description}`,
171
+ );
172
+ } catch (error) {
173
+ await db.exec("ROLLBACK");
174
+ const err = error as Error;
175
+ console.error(
176
+ `[migrations] Failed to apply migration ${migration.version}: ${err.message}`,
177
+ );
178
+ throw new Error(`Migration ${migration.version} failed: ${err.message}`);
179
+ }
180
+ }
181
+
182
+ const finalVersion = await getCurrentVersion(db);
183
+ return { applied, current: finalVersion };
184
+ }
185
+
186
+ /**
187
+ * Rollback to a specific version
188
+ *
189
+ * WARNING: This will DROP tables and LOSE DATA.
190
+ * Only use for testing or emergency recovery.
191
+ */
192
+ export async function rollbackTo(
193
+ db: PGlite,
194
+ targetVersion: number,
195
+ ): Promise<{
196
+ rolledBack: number[];
197
+ current: number;
198
+ }> {
199
+ const currentVersion = await getCurrentVersion(db);
200
+ const rolledBack: number[] = [];
201
+
202
+ if (targetVersion >= currentVersion) {
203
+ return { rolledBack: [], current: currentVersion };
204
+ }
205
+
206
+ // Find migrations to rollback (in reverse order)
207
+ const migrationsToRollback = migrations
208
+ .filter((m) => m.version > targetVersion && m.version <= currentVersion)
209
+ .sort((a, b) => b.version - a.version); // Descending order
210
+
211
+ for (const migration of migrationsToRollback) {
212
+ await db.exec("BEGIN");
213
+ try {
214
+ // Run the down migration
215
+ await db.exec(migration.down);
216
+
217
+ // Remove from version table
218
+ await db.query(`DELETE FROM schema_version WHERE version = $1`, [
219
+ migration.version,
220
+ ]);
221
+
222
+ await db.exec("COMMIT");
223
+ rolledBack.push(migration.version);
224
+
225
+ console.log(
226
+ `[migrations] Rolled back migration ${migration.version}: ${migration.description}`,
227
+ );
228
+ } catch (error) {
229
+ await db.exec("ROLLBACK");
230
+ const err = error as Error;
231
+ console.error(
232
+ `[migrations] Failed to rollback migration ${migration.version}: ${err.message}`,
233
+ );
234
+ throw new Error(
235
+ `Rollback of migration ${migration.version} failed: ${err.message}`,
236
+ );
237
+ }
238
+ }
239
+
240
+ const finalVersion = await getCurrentVersion(db);
241
+ return { rolledBack, current: finalVersion };
242
+ }
243
+
244
+ /**
245
+ * Check if a specific migration has been applied
246
+ */
247
+ export async function isMigrationApplied(
248
+ db: PGlite,
249
+ version: number,
250
+ ): Promise<boolean> {
251
+ await ensureVersionTable(db);
252
+
253
+ const result = await db.query<{ count: string }>(
254
+ `SELECT COUNT(*) as count FROM schema_version WHERE version = $1`,
255
+ [version],
256
+ );
257
+
258
+ return parseInt(result.rows[0]?.count || "0") > 0;
259
+ }
260
+
261
+ /**
262
+ * Get pending migrations (not yet applied)
263
+ */
264
+ export async function getPendingMigrations(db: PGlite): Promise<Migration[]> {
265
+ const currentVersion = await getCurrentVersion(db);
266
+ return migrations
267
+ .filter((m) => m.version > currentVersion)
268
+ .sort((a, b) => a.version - b.version);
269
+ }