@vibesdotdev/localdb 0.0.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.
Files changed (130) hide show
  1. package/README.md +121 -0
  2. package/SPEC.md +119 -0
  3. package/dist/cli/commands/_shared.consumer.d.ts +5 -0
  4. package/dist/cli/commands/_shared.consumer.d.ts.map +1 -0
  5. package/dist/cli/commands/_shared.consumer.js +17 -0
  6. package/dist/cli/commands/_shared.consumer.js.map +1 -0
  7. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.d.ts +19 -0
  8. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.d.ts.map +1 -0
  9. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.js +19 -0
  10. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.js.map +1 -0
  11. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.d.ts +5 -0
  12. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.d.ts.map +1 -0
  13. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.js +80 -0
  14. package/dist/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.js.map +1 -0
  15. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.d.ts +19 -0
  16. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.d.ts.map +1 -0
  17. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.js +16 -0
  18. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.js.map +1 -0
  19. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.d.ts +5 -0
  20. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.d.ts.map +1 -0
  21. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.js +33 -0
  22. package/dist/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.js.map +1 -0
  23. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.d.ts +15 -0
  24. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.d.ts.map +1 -0
  25. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.js +15 -0
  26. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.js.map +1 -0
  27. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.d.ts +5 -0
  28. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.d.ts.map +1 -0
  29. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.js +28 -0
  30. package/dist/cli/commands/pools/dev.localdb.pools.cli-command.impl.js.map +1 -0
  31. package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.d.ts +24 -0
  32. package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.d.ts.map +1 -0
  33. package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.js +20 -0
  34. package/dist/cli/commands/query/dev.localdb.query.cli-command.descriptor.js.map +1 -0
  35. package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.d.ts +5 -0
  36. package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.d.ts.map +1 -0
  37. package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.js +63 -0
  38. package/dist/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.js.map +1 -0
  39. package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.d.ts +19 -0
  40. package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.d.ts.map +1 -0
  41. package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.js +19 -0
  42. package/dist/cli/commands/status/dev.localdb.status.cli-command.descriptor.js.map +1 -0
  43. package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.d.ts +5 -0
  44. package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.d.ts.map +1 -0
  45. package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.js +73 -0
  46. package/dist/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.js.map +1 -0
  47. package/dist/core/connection-pool.d.ts +84 -0
  48. package/dist/core/connection-pool.d.ts.map +1 -0
  49. package/dist/core/connection-pool.js +191 -0
  50. package/dist/core/connection-pool.js.map +1 -0
  51. package/dist/core/database.d.ts +137 -0
  52. package/dist/core/database.d.ts.map +1 -0
  53. package/dist/core/database.js +347 -0
  54. package/dist/core/database.js.map +1 -0
  55. package/dist/core/error-context.d.ts +2 -0
  56. package/dist/core/error-context.d.ts.map +1 -0
  57. package/dist/core/error-context.js +17 -0
  58. package/dist/core/error-context.js.map +1 -0
  59. package/dist/core/migration-registry.d.ts +89 -0
  60. package/dist/core/migration-registry.d.ts.map +1 -0
  61. package/dist/core/migration-registry.js +226 -0
  62. package/dist/core/migration-registry.js.map +1 -0
  63. package/dist/core/runtime.d.ts +3 -0
  64. package/dist/core/runtime.d.ts.map +1 -0
  65. package/dist/core/runtime.js +17 -0
  66. package/dist/core/runtime.js.map +1 -0
  67. package/dist/dev.localdb.cli-group.descriptor.d.ts +9 -0
  68. package/dist/dev.localdb.cli-group.descriptor.d.ts.map +1 -0
  69. package/dist/dev.localdb.cli-group.descriptor.js +17 -0
  70. package/dist/dev.localdb.cli-group.descriptor.js.map +1 -0
  71. package/dist/dev.localdb.context.descriptor.d.ts +21 -0
  72. package/dist/dev.localdb.context.descriptor.d.ts.map +1 -0
  73. package/dist/dev.localdb.context.descriptor.js +12 -0
  74. package/dist/dev.localdb.context.descriptor.js.map +1 -0
  75. package/dist/dev.localdb.context.impl.consumer.d.ts +9 -0
  76. package/dist/dev.localdb.context.impl.consumer.d.ts.map +1 -0
  77. package/dist/dev.localdb.context.impl.consumer.js +10 -0
  78. package/dist/dev.localdb.context.impl.consumer.js.map +1 -0
  79. package/dist/index.d.ts +12 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +11 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/localdb.cloud.plugin.d.ts +17 -0
  84. package/dist/localdb.cloud.plugin.d.ts.map +1 -0
  85. package/dist/localdb.cloud.plugin.js +23 -0
  86. package/dist/localdb.cloud.plugin.js.map +1 -0
  87. package/dist/localdb.plugin.d.ts +8 -0
  88. package/dist/localdb.plugin.d.ts.map +1 -0
  89. package/dist/localdb.plugin.js +38 -0
  90. package/dist/localdb.plugin.js.map +1 -0
  91. package/dist/migrations/load-sql.d.ts +45 -0
  92. package/dist/migrations/load-sql.d.ts.map +1 -0
  93. package/dist/migrations/load-sql.js +117 -0
  94. package/dist/migrations/load-sql.js.map +1 -0
  95. package/dist/schemas/api.d.ts +82 -0
  96. package/dist/schemas/api.d.ts.map +1 -0
  97. package/dist/schemas/api.js +2 -0
  98. package/dist/schemas/api.js.map +1 -0
  99. package/package.json +146 -0
  100. package/src/cli/commands/_shared.consumer.ts +20 -0
  101. package/src/cli/commands/migrate/dev.localdb.migrate.cli-command.descriptor.ts +20 -0
  102. package/src/cli/commands/migrate/dev.localdb.migrate.cli-command.impl.consumer.ts +97 -0
  103. package/src/cli/commands/namespaces/dev.localdb.namespaces.cli-command.descriptor.ts +17 -0
  104. package/src/cli/commands/namespaces/dev.localdb.namespaces.cli-command.impl.ts +46 -0
  105. package/src/cli/commands/pools/dev.localdb.pools.cli-command.descriptor.ts +16 -0
  106. package/src/cli/commands/pools/dev.localdb.pools.cli-command.impl.ts +37 -0
  107. package/src/cli/commands/query/dev.localdb.query.cli-command.descriptor.ts +21 -0
  108. package/src/cli/commands/query/dev.localdb.query.cli-command.impl.consumer.ts +69 -0
  109. package/src/cli/commands/status/dev.localdb.status.cli-command.descriptor.ts +20 -0
  110. package/src/cli/commands/status/dev.localdb.status.cli-command.impl.consumer.ts +93 -0
  111. package/src/core/connection-pool.ts +240 -0
  112. package/src/core/database.ts +419 -0
  113. package/src/core/error-context.ts +19 -0
  114. package/src/core/migration-registry.ts +321 -0
  115. package/src/core/runtime.ts +17 -0
  116. package/src/dev.localdb.cli-group.descriptor.ts +20 -0
  117. package/src/dev.localdb.context.descriptor.ts +13 -0
  118. package/src/dev.localdb.context.impl.consumer.ts +12 -0
  119. package/src/index.ts +28 -0
  120. package/src/localdb.cloud.plugin.ts +24 -0
  121. package/src/localdb.plugin.ts +43 -0
  122. package/src/migrations/atlas/001-initial-schema.sql +173 -0
  123. package/src/migrations/atlas/002-lang-server-fields.sql +12 -0
  124. package/src/migrations/atlas/003-config-id-dedup.sql +31 -0
  125. package/src/migrations/atlas/004-fix-on-conflict-constraints.sql +25 -0
  126. package/src/migrations/atlas/005-diagnostics.sql +66 -0
  127. package/src/migrations/atlas/006-diagnostic-summaries.sql +65 -0
  128. package/src/migrations/atlas/007-diagnostic-slice.sql +83 -0
  129. package/src/migrations/load-sql.ts +133 -0
  130. package/src/schemas/api.ts +92 -0
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Local Database Manager
3
+ *
4
+ * High-level API for managing local SQLite databases in vibes dev.
5
+ * Provides connection pooling, migration management, and consistent configuration.
6
+ *
7
+ * Usage:
8
+ * const db = localdb.getDatabase('atlas', '.vibes/atlas/project.db');
9
+ * // db is ready to use with WAL mode, busy timeout, and migrations applied
10
+ */
11
+
12
+ import { Database } from 'bun:sqlite';
13
+ import { getLogger } from '@vibesdotdev/logging';
14
+ import { connectionPool } from './connection-pool.ts';
15
+ import { migrationRegistry, type Migration } from './migration-registry.ts';
16
+ import { toErrorContext } from './error-context';
17
+ import { isLocaldbServerRuntime } from './runtime.ts';
18
+
19
+ const logger = getLogger('localdb:database');
20
+
21
+ let mkdir: typeof import('node:fs/promises').mkdir;
22
+ let copyFile: typeof import('node:fs/promises').copyFile;
23
+ let unlink: typeof import('node:fs/promises').unlink;
24
+ let readdir: typeof import('node:fs/promises').readdir;
25
+ let dirname: typeof import('node:path').dirname;
26
+ let basename: typeof import('node:path').basename;
27
+
28
+ const BACKUP_RETENTION_COUNT = 12;
29
+ const isServerRuntime = isLocaldbServerRuntime();
30
+
31
+ if (isServerRuntime) {
32
+ ({ mkdir, copyFile, unlink, readdir } = await import('node:fs/promises'));
33
+ ({ dirname, basename } = await import('node:path'));
34
+ }
35
+
36
+ export interface DatabaseOptions {
37
+ /** Namespace for migrations (e.g., 'atlas', 'session') */
38
+ namespace: string;
39
+
40
+ /** Path to database file */
41
+ dbPath: string;
42
+
43
+ /** Consumer identifier for debugging */
44
+ consumer?: string;
45
+
46
+ /** Skip migration check/run on initialization */
47
+ skipMigrations?: boolean;
48
+
49
+ /** Use connection pool (default: true) */
50
+ usePool?: boolean;
51
+ }
52
+
53
+ interface NodeLikeError {
54
+ code?: string;
55
+ }
56
+
57
+ function isFileMissingError(error: unknown): boolean {
58
+ if (!error || typeof error !== 'object') return false;
59
+ return (error as NodeLikeError).code === 'ENOENT';
60
+ }
61
+
62
+ export class LocalDatabase {
63
+ private db!: Database;
64
+ private options: Required<DatabaseOptions>;
65
+ private initialized = false;
66
+
67
+ constructor(options: DatabaseOptions) {
68
+ this.options = {
69
+ consumer: 'unknown',
70
+ skipMigrations: false,
71
+ usePool: true,
72
+ ...options
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Initialize the database connection
78
+ * - Creates directory if needed
79
+ * - Gets/creates pooled connection
80
+ * - Runs pending migrations
81
+ */
82
+ async initialize(): Promise<void> {
83
+ if (this.initialized) return;
84
+
85
+ // Ensure directory exists
86
+ if (!isServerRuntime) {
87
+ throw new Error('LocalDatabase is not available in browser builds');
88
+ }
89
+ await mkdir(dirname(this.options.dbPath), { recursive: true });
90
+
91
+ // Get connection (pooled or direct)
92
+ this.establishConnection();
93
+
94
+ // Run migrations if needed
95
+ if (!this.options.skipMigrations) {
96
+ try {
97
+ // Check for incompatible database from old migration system
98
+ if (migrationRegistry.isIncompatibleDatabase(this.db, this.options.namespace)) {
99
+ logger.error(
100
+ `[LocalDB] Detected incompatible database for ${this.options.namespace} (from legacy migration system). Initiating recovery...`
101
+ );
102
+ await this.performRecovery();
103
+ } else if (migrationRegistry.needsMigration(this.db, this.options.namespace)) {
104
+ await migrationRegistry.runMigrations(this.db, this.options.namespace);
105
+ }
106
+ } catch (error) {
107
+ logger.error(
108
+ `[LocalDB] Migration failed for ${this.options.namespace}. Initiating automatic recovery/trapdoor...`,
109
+ toErrorContext(error)
110
+ );
111
+
112
+ await this.performRecovery();
113
+ }
114
+ }
115
+
116
+ this.initialized = true;
117
+ }
118
+
119
+ /**
120
+ * Establish initial connection
121
+ */
122
+ private establishConnection(): void {
123
+ if (this.options.usePool) {
124
+ this.db = connectionPool.getConnection(this.options.dbPath, this.options.consumer);
125
+ } else {
126
+ // Direct connection (for special cases)
127
+ this.db = new Database(this.options.dbPath);
128
+ this.configureDatabase(this.db);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Perform recovery from migration failure
134
+ * 1. Close connection
135
+ * 2. Backup corrupted DB
136
+ * 3. Delete corrupted DB
137
+ * 4. Re-initialize fresh DB
138
+ * 5. Re-run migrations
139
+ */
140
+ private async performRecovery(): Promise<void> {
141
+ // 1. Close the broken database
142
+ if (this.options.usePool) {
143
+ connectionPool.closeConnection(this.options.dbPath, true);
144
+ } else {
145
+ this.db.close();
146
+ }
147
+
148
+ // 2. Backup the broken file
149
+ const backupPath = `${this.options.dbPath}.bak-${Date.now()}`;
150
+ try {
151
+ await copyFile(this.options.dbPath, backupPath);
152
+ logger.info(`[LocalDB] Backed up corrupted database to: ${backupPath}`);
153
+ await this.pruneBackupFiles();
154
+ } catch (backupError) {
155
+ logger.error(
156
+ '[LocalDB] Failed to backup corrupted database:',
157
+ toErrorContext(backupError)
158
+ );
159
+ }
160
+
161
+ // 3. Delete the broken file and WAL artifacts
162
+ try {
163
+ await unlink(this.options.dbPath);
164
+ await unlink(`${this.options.dbPath}-wal`).catch(() => {});
165
+ await unlink(`${this.options.dbPath}-shm`).catch(() => {});
166
+ logger.info('[LocalDB] Deleted corrupted database file.');
167
+ } catch (deleteError) {
168
+ if (isFileMissingError(deleteError)) {
169
+ logger.info(
170
+ '[LocalDB] Corrupted database file already removed during recovery. Continuing...'
171
+ );
172
+ } else {
173
+ logger.error(
174
+ '[LocalDB] Failed to delete corrupted database:',
175
+ toErrorContext(deleteError)
176
+ );
177
+ throw deleteError; // If we can't delete, we can't recover.
178
+ }
179
+ }
180
+
181
+ // 4. Re-establish connection (creates new DB file)
182
+ logger.info('[LocalDB] Re-initializing fresh database...');
183
+ this.establishConnection();
184
+
185
+ // 5. Re-run migrations on fresh DB
186
+ try {
187
+ if (migrationRegistry.needsMigration(this.db, this.options.namespace)) {
188
+ await migrationRegistry.runMigrations(this.db, this.options.namespace);
189
+ }
190
+ logger.info('[LocalDB] Recovery successful. Database reset.');
191
+ } catch (retryError) {
192
+ logger.error(
193
+ '[LocalDB] Recovery failed. Fresh database migration failed:',
194
+ toErrorContext(retryError)
195
+ );
196
+ throw retryError;
197
+ }
198
+ }
199
+
200
+ private async pruneBackupFiles(): Promise<void> {
201
+ const directory = dirname(this.options.dbPath);
202
+ const filename = basename(this.options.dbPath);
203
+ const backupPrefix = `${filename}.bak-`;
204
+ const files = await readdir(directory).catch(() => []);
205
+ const backups = files
206
+ .filter((entry) => entry.startsWith(backupPrefix))
207
+ .map((entry) => ({ entry, timestamp: Number(entry.slice(backupPrefix.length)) }))
208
+ .sort((a, b) => {
209
+ if (Number.isNaN(a.timestamp) || Number.isNaN(b.timestamp)) {
210
+ return b.entry.localeCompare(a.entry);
211
+ }
212
+ return b.timestamp - a.timestamp;
213
+ });
214
+
215
+ for (const backup of backups.slice(BACKUP_RETENTION_COUNT)) {
216
+ await unlink(`${directory}/${backup.entry}`).catch(() => {});
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Configure database with optimal settings (for non-pooled connections)
222
+ */
223
+ private configureDatabase(db: Database): void {
224
+ db.exec('PRAGMA journal_mode = WAL;');
225
+ db.exec('PRAGMA busy_timeout = 5000;');
226
+ db.exec('PRAGMA foreign_keys = ON;');
227
+ db.exec('PRAGMA synchronous = NORMAL;');
228
+ db.exec('PRAGMA temp_store = MEMORY;');
229
+ }
230
+
231
+ /**
232
+ * Get the underlying Database instance
233
+ */
234
+ getDatabase(): Database {
235
+ if (!this.initialized) {
236
+ throw new Error('Database not initialized. Call initialize() first.');
237
+ }
238
+ return this.db;
239
+ }
240
+
241
+ /**
242
+ * Close the database connection
243
+ */
244
+ close(): void {
245
+ if (!this.initialized) return;
246
+
247
+ if (this.options.usePool) {
248
+ // Release back to pool
249
+ connectionPool.releaseConnection(this.options.dbPath);
250
+ } else {
251
+ // Direct close
252
+ this.db.close();
253
+ }
254
+
255
+ this.initialized = false;
256
+ }
257
+
258
+ /**
259
+ * Check if database is healthy
260
+ */
261
+ healthCheck(): boolean {
262
+ if (!this.initialized) return false;
263
+
264
+ try {
265
+ const result = this.db.query('SELECT 1').get();
266
+ return result !== null;
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Get migration status for this database
274
+ */
275
+ getMigrationStatus() {
276
+ if (!this.initialized) {
277
+ throw new Error('Database not initialized');
278
+ }
279
+
280
+ return migrationRegistry.getDatabaseStatus(this.db, this.options.namespace);
281
+ }
282
+ }
283
+
284
+ /**
285
+ * LocalDB Manager
286
+ * Main entry point for accessing local databases
287
+ */
288
+ class LocalDBManager {
289
+ private databases = new Map<
290
+ string,
291
+ {
292
+ database: LocalDatabase;
293
+ references: number;
294
+ }
295
+ >();
296
+ private dbPathLocks = new Map<string, Promise<void>>();
297
+
298
+ private async withDbPathLock<T>(dbPath: string, task: () => Promise<T>): Promise<T> {
299
+ const prior = this.dbPathLocks.get(dbPath) ?? Promise.resolve();
300
+ let release: (() => void) | undefined;
301
+ const current = new Promise<void>((resolve) => {
302
+ release = resolve;
303
+ });
304
+ const queued = prior.then(() => current);
305
+ this.dbPathLocks.set(dbPath, queued);
306
+ await prior;
307
+ try {
308
+ return await task();
309
+ } finally {
310
+ release?.();
311
+ if (this.dbPathLocks.get(dbPath) === queued) {
312
+ this.dbPathLocks.delete(dbPath);
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Register migrations for a namespace
319
+ * Must be called before getDatabase() for that namespace
320
+ */
321
+ registerMigrations(namespace: string, migrations: Migration[]): void {
322
+ migrationRegistry.register(namespace, migrations);
323
+ }
324
+
325
+ /**
326
+ * Get or create a database instance
327
+ *
328
+ * @param namespace Migration namespace (e.g., 'atlas', 'session')
329
+ * @param dbPath Path to database file
330
+ * @param consumer Consumer identifier for debugging
331
+ * @returns Initialized database instance
332
+ */
333
+ getDatabase(namespace: string, dbPath: string, consumer?: string): Promise<LocalDatabase> {
334
+ const key = `${namespace}:${dbPath}`;
335
+ return this.withDbPathLock(dbPath, async () => {
336
+ const existing = this.databases.get(key);
337
+ if (existing) {
338
+ existing.references += 1;
339
+ return existing.database;
340
+ }
341
+
342
+ if (!this.databases.has(key)) {
343
+ const db = new LocalDatabase({
344
+ namespace,
345
+ dbPath,
346
+ consumer
347
+ });
348
+
349
+ await db.initialize();
350
+ this.databases.set(key, {
351
+ database: db,
352
+ references: 1
353
+ });
354
+ }
355
+
356
+ return this.databases.get(key)!.database;
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Get a raw Database instance (auto-configured with WAL, etc.)
362
+ * For when you don't need migration management
363
+ */
364
+ getRawDatabase(dbPath: string, consumer = 'raw'): Database {
365
+ return connectionPool.getConnection(dbPath, consumer);
366
+ }
367
+
368
+ /**
369
+ * Release a database instance
370
+ */
371
+ async releaseDatabase(namespace: string, dbPath: string): Promise<void> {
372
+ const key = `${namespace}:${dbPath}`;
373
+ await this.withDbPathLock(dbPath, async () => {
374
+ const entry = this.databases.get(key);
375
+ if (!entry) return;
376
+
377
+ entry.references = Math.max(0, entry.references - 1);
378
+ if (entry.references === 0) {
379
+ await entry.database.close();
380
+ this.databases.delete(key);
381
+ }
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Get connection pool statistics
387
+ */
388
+ getPoolStats() {
389
+ return connectionPool.getStats();
390
+ }
391
+
392
+ /**
393
+ * Get migration registry status
394
+ */
395
+ getMigrationStatus() {
396
+ return migrationRegistry.getStatus();
397
+ }
398
+
399
+ /**
400
+ * List all registered migration namespaces
401
+ */
402
+ listNamespaces(): string[] {
403
+ return migrationRegistry.listNamespaces();
404
+ }
405
+
406
+ /**
407
+ * Close all database connections
408
+ */
409
+ async closeAll(): Promise<void> {
410
+ for (const entry of this.databases.values()) {
411
+ await entry.database.close();
412
+ }
413
+ this.databases.clear();
414
+ connectionPool.closeAll();
415
+ }
416
+ }
417
+
418
+ // Singleton instance
419
+ export const localdb = new LocalDBManager();
@@ -0,0 +1,19 @@
1
+ export function toErrorContext(error: unknown): Record<string, unknown> | undefined {
2
+ if (error instanceof Error) {
3
+ return {
4
+ error: error.message,
5
+ name: error.name,
6
+ stack: error.stack
7
+ };
8
+ }
9
+
10
+ if (typeof error === 'object' && error !== null && !Array.isArray(error)) {
11
+ return error as Record<string, unknown>;
12
+ }
13
+
14
+ if (error === undefined) {
15
+ return undefined;
16
+ }
17
+
18
+ return { error: String(error) };
19
+ }