spindb 0.36.2 → 0.37.2

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 (40) hide show
  1. package/README.md +19 -8
  2. package/cli/commands/create.ts +9 -2
  3. package/cli/commands/databases.ts +17 -12
  4. package/cli/commands/delete.ts +3 -0
  5. package/cli/commands/engines.ts +60 -4
  6. package/cli/commands/info.ts +5 -0
  7. package/cli/commands/list.ts +2 -0
  8. package/cli/commands/menu/backup-handlers.ts +2 -0
  9. package/cli/commands/menu/settings-handlers.ts +3 -0
  10. package/cli/commands/menu/shell-handlers.ts +23 -0
  11. package/cli/commands/menu/update-handlers.ts +6 -2
  12. package/cli/commands/restore.ts +3 -0
  13. package/cli/commands/start.ts +3 -0
  14. package/cli/commands/url.ts +4 -0
  15. package/cli/constants.ts +4 -0
  16. package/cli/helpers.ts +93 -0
  17. package/config/backup-formats.ts +14 -0
  18. package/config/engine-defaults.ts +13 -0
  19. package/config/engines.json +17 -0
  20. package/core/config-manager.ts +5 -0
  21. package/core/dependency-manager.ts +2 -0
  22. package/core/docker-exporter.ts +17 -0
  23. package/core/library-env.ts +2 -4
  24. package/core/update-manager.ts +57 -35
  25. package/engines/base-engine.ts +8 -0
  26. package/engines/index.ts +4 -0
  27. package/engines/mariadb/index.ts +5 -4
  28. package/engines/redis/index.ts +15 -4
  29. package/engines/tigerbeetle/README.md +61 -0
  30. package/engines/tigerbeetle/backup.ts +49 -0
  31. package/engines/tigerbeetle/binary-manager.ts +95 -0
  32. package/engines/tigerbeetle/binary-urls.ts +62 -0
  33. package/engines/tigerbeetle/hostdb-releases.ts +26 -0
  34. package/engines/tigerbeetle/index.ts +746 -0
  35. package/engines/tigerbeetle/restore.ts +130 -0
  36. package/engines/tigerbeetle/version-maps.ts +68 -0
  37. package/engines/tigerbeetle/version-validator.ts +126 -0
  38. package/engines/valkey/index.ts +15 -4
  39. package/package.json +2 -1
  40. package/types/index.ts +9 -0
package/cli/helpers.ts CHANGED
@@ -5,9 +5,28 @@ import { execFile } from 'child_process'
5
5
  import { promisify } from 'util'
6
6
  import { paths } from '../config/paths'
7
7
  import { platformService } from '../core/platform-service'
8
+ import { type Engine } from '../types'
9
+ import { getEngineConfig } from '../config/engines-registry'
8
10
 
9
11
  const execFileAsync = promisify(execFile)
10
12
 
13
+ export type EngineMetadata = {
14
+ queryLanguage: string
15
+ runtime: 'server' | 'embedded'
16
+ connectionScheme: string | null
17
+ }
18
+
19
+ export async function getEngineMetadata(
20
+ engine: string,
21
+ ): Promise<EngineMetadata> {
22
+ const config = await getEngineConfig(engine as Engine)
23
+ return {
24
+ queryLanguage: config.queryLanguage,
25
+ runtime: config.runtime,
26
+ connectionScheme: config.connectionScheme,
27
+ }
28
+ }
29
+
11
30
  // Parsed engine directory info
12
31
  type ParsedEngineDir = {
13
32
  version: string
@@ -254,6 +273,16 @@ export type InstalledWeaviateEngine = {
254
273
  source: 'downloaded'
255
274
  }
256
275
 
276
+ export type InstalledTigerBeetleEngine = {
277
+ engine: 'tigerbeetle'
278
+ version: string
279
+ platform: string
280
+ arch: string
281
+ path: string
282
+ sizeBytes: number
283
+ source: 'downloaded'
284
+ }
285
+
257
286
  export type InstalledEngine =
258
287
  | InstalledPostgresEngine
259
288
  | InstalledMariadbEngine
@@ -274,6 +303,7 @@ export type InstalledEngine =
274
303
  | InstalledTypeDBEngine
275
304
  | InstalledInfluxDBEngine
276
305
  | InstalledWeaviateEngine
306
+ | InstalledTigerBeetleEngine
277
307
 
278
308
  async function getPostgresVersion(binPath: string): Promise<string | null> {
279
309
  const ext = platformService.getExecutableExtension()
@@ -1347,6 +1377,64 @@ async function getInstalledWeaviateEngines(): Promise<
1347
1377
  return engines
1348
1378
  }
1349
1379
 
1380
+ // Get TigerBeetle version from binary path
1381
+ async function getTigerBeetleVersion(binPath: string): Promise<string | null> {
1382
+ const ext = platformService.getExecutableExtension()
1383
+ const tigerbeetlePath = join(binPath, 'bin', `tigerbeetle${ext}`)
1384
+ if (!existsSync(tigerbeetlePath)) {
1385
+ return null
1386
+ }
1387
+
1388
+ try {
1389
+ const { stdout } = await execFileAsync(tigerbeetlePath, ['version'])
1390
+ // Parse output like "TigerBeetle v0.16.70" or "0.16.70"
1391
+ const match = stdout.match(/(?:TigerBeetle\s+)?v?(\d+\.\d+\.\d+)/)
1392
+ return match ? match[1] : null
1393
+ } catch {
1394
+ return null
1395
+ }
1396
+ }
1397
+
1398
+ // Get installed TigerBeetle engines from downloaded binaries
1399
+ async function getInstalledTigerBeetleEngines(): Promise<
1400
+ InstalledTigerBeetleEngine[]
1401
+ > {
1402
+ const binDir = paths.bin
1403
+
1404
+ if (!existsSync(binDir)) {
1405
+ return []
1406
+ }
1407
+
1408
+ const entries = await readdir(binDir, { withFileTypes: true })
1409
+ const engines: InstalledTigerBeetleEngine[] = []
1410
+
1411
+ for (const entry of entries) {
1412
+ if (!entry.isDirectory()) continue
1413
+ if (!entry.name.startsWith('tigerbeetle-')) continue
1414
+
1415
+ const parsed = parseEngineDirectory(entry.name, 'tigerbeetle-', binDir)
1416
+ if (!parsed) continue
1417
+
1418
+ const actualVersion =
1419
+ (await getTigerBeetleVersion(parsed.path)) || parsed.version
1420
+ const sizeBytes = await calculateDirectorySize(parsed.path)
1421
+
1422
+ engines.push({
1423
+ engine: 'tigerbeetle',
1424
+ version: actualVersion,
1425
+ platform: parsed.platform,
1426
+ arch: parsed.arch,
1427
+ path: parsed.path,
1428
+ sizeBytes,
1429
+ source: 'downloaded',
1430
+ })
1431
+ }
1432
+
1433
+ engines.sort((a, b) => compareVersions(b.version, a.version))
1434
+
1435
+ return engines
1436
+ }
1437
+
1350
1438
  export function compareVersions(a: string, b: string): number {
1351
1439
  const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
1352
1440
  const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
@@ -1384,6 +1472,7 @@ const ENGINE_PREFIXES = [
1384
1472
  'typedb-',
1385
1473
  'influxdb-',
1386
1474
  'weaviate-',
1475
+ 'tigerbeetle-',
1387
1476
  ] as const
1388
1477
 
1389
1478
  /**
@@ -1433,6 +1522,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
1433
1522
  typedbEngines,
1434
1523
  influxdbEngines,
1435
1524
  weaviateEngines,
1525
+ tigerbeetleEngines,
1436
1526
  ] = await Promise.all([
1437
1527
  getInstalledPostgresEngines(),
1438
1528
  getInstalledMariadbEngines(),
@@ -1453,6 +1543,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
1453
1543
  getInstalledTypeDBEngines(),
1454
1544
  getInstalledInfluxDBEngines(),
1455
1545
  getInstalledWeaviateEngines(),
1546
+ getInstalledTigerBeetleEngines(),
1456
1547
  ])
1457
1548
 
1458
1549
  return [
@@ -1475,6 +1566,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
1475
1566
  ...typedbEngines,
1476
1567
  ...influxdbEngines,
1477
1568
  ...weaviateEngines,
1569
+ ...tigerbeetleEngines,
1478
1570
  ]
1479
1571
  }
1480
1572
 
@@ -1497,4 +1589,5 @@ export {
1497
1589
  getInstalledTypeDBEngines,
1498
1590
  getInstalledInfluxDBEngines,
1499
1591
  getInstalledWeaviateEngines,
1592
+ getInstalledTigerBeetleEngines,
1500
1593
  }
@@ -29,6 +29,7 @@ import {
29
29
  type TypeDBFormat,
30
30
  type InfluxDBFormat,
31
31
  type WeaviateFormat,
32
+ type TigerBeetleFormat,
32
33
  type BackupFormatType,
33
34
  } from '../types'
34
35
 
@@ -67,6 +68,7 @@ export const BACKUP_FORMATS: {
67
68
  [Engine.TypeDB]: EngineBackupFormats<TypeDBFormat>
68
69
  [Engine.InfluxDB]: EngineBackupFormats<InfluxDBFormat>
69
70
  [Engine.Weaviate]: EngineBackupFormats<WeaviateFormat>
71
+ [Engine.TigerBeetle]: EngineBackupFormats<TigerBeetleFormat>
70
72
  } = {
71
73
  [Engine.PostgreSQL]: {
72
74
  formats: {
@@ -350,6 +352,18 @@ export const BACKUP_FORMATS: {
350
352
  supportsFormatChoice: false, // Only snapshot format supported
351
353
  defaultFormat: 'snapshot',
352
354
  },
355
+ [Engine.TigerBeetle]: {
356
+ formats: {
357
+ binary: {
358
+ extension: '.tigerbeetle',
359
+ label: '.tigerbeetle',
360
+ description: 'TigerBeetle data file - full database copy',
361
+ spinnerLabel: 'binary',
362
+ },
363
+ },
364
+ supportsFormatChoice: false, // Only binary format supported
365
+ defaultFormat: 'binary',
366
+ },
353
367
  }
354
368
 
355
369
  /**
@@ -278,6 +278,19 @@ export const engineDefaults: Record<Engine, EngineDefaults> = {
278
278
  clientTools: [], // Weaviate uses REST/GraphQL API, no separate CLI tools
279
279
  maxConnections: 0, // Not applicable for vector DB
280
280
  },
281
+ [Engine.TigerBeetle]: {
282
+ defaultVersion: '0.16',
283
+ defaultPort: 3000,
284
+ portRange: { start: 3000, end: 3100 },
285
+ latestVersion: '0.16',
286
+ superuser: '', // No auth
287
+ connectionScheme: '', // Custom binary protocol, no URI scheme
288
+ logFileName: 'tigerbeetle.log',
289
+ pidFileName: 'tigerbeetle.pid',
290
+ dataSubdir: 'data',
291
+ clientTools: ['tigerbeetle'], // Single binary serves as both server and REPL
292
+ maxConnections: 0, // Not applicable
293
+ },
281
294
  }
282
295
 
283
296
  /**
@@ -326,6 +326,23 @@
326
326
  "clientTools": ["weaviate"],
327
327
  "licensing": "BSD-3-Clause",
328
328
  "notes": "AI-native vector database. REST API on port 8080, gRPC on port+1. Uses classes/collections instead of databases."
329
+ },
330
+ "tigerbeetle": {
331
+ "displayName": "TigerBeetle",
332
+ "icon": "🐯",
333
+ "status": "integrated",
334
+ "binarySource": "hostdb",
335
+ "supportedVersions": ["0.16.70"],
336
+ "defaultVersion": "0.16.70",
337
+ "defaultPort": 3000,
338
+ "runtime": "server",
339
+ "queryLanguage": "custom",
340
+ "scriptFileLabel": null,
341
+ "connectionScheme": null,
342
+ "superuser": null,
343
+ "clientTools": ["tigerbeetle"],
344
+ "licensing": "Apache-2.0",
345
+ "notes": "High-performance financial ledger database. Custom binary protocol, REPL client. Uses --development flag for local dev."
329
346
  }
330
347
  }
331
348
  }
@@ -84,6 +84,8 @@ const INFLUXDB_TOOLS: BinaryTool[] = ['influxdb3']
84
84
 
85
85
  const WEAVIATE_TOOLS: BinaryTool[] = ['weaviate']
86
86
 
87
+ const TIGERBEETLE_TOOLS: BinaryTool[] = ['tigerbeetle']
88
+
87
89
  const PGWEB_TOOLS: BinaryTool[] = ['pgweb']
88
90
 
89
91
  const DBLAB_TOOLS: BinaryTool[] = ['dblab']
@@ -113,6 +115,7 @@ const ALL_TOOLS: BinaryTool[] = [
113
115
  ...TYPEDB_TOOLS,
114
116
  ...INFLUXDB_TOOLS,
115
117
  ...WEAVIATE_TOOLS,
118
+ ...TIGERBEETLE_TOOLS,
116
119
  ...PGWEB_TOOLS,
117
120
  ...DBLAB_TOOLS,
118
121
  ...SQLITE_TOOLS,
@@ -139,6 +142,7 @@ const ENGINE_BINARY_MAP: Partial<Record<Engine, BinaryTool[]>> = {
139
142
  [Engine.TypeDB]: TYPEDB_TOOLS,
140
143
  [Engine.InfluxDB]: INFLUXDB_TOOLS,
141
144
  [Engine.Weaviate]: WEAVIATE_TOOLS,
145
+ [Engine.TigerBeetle]: TIGERBEETLE_TOOLS,
142
146
  }
143
147
 
144
148
  export class ConfigManager {
@@ -637,6 +641,7 @@ export {
637
641
  TYPEDB_TOOLS,
638
642
  INFLUXDB_TOOLS,
639
643
  WEAVIATE_TOOLS,
644
+ TIGERBEETLE_TOOLS,
640
645
  PGWEB_TOOLS,
641
646
  DBLAB_TOOLS,
642
647
  SQLITE_TOOLS,
@@ -88,6 +88,8 @@ const KNOWN_BINARY_TOOLS: readonly BinaryTool[] = [
88
88
  'influxdb3',
89
89
  // Weaviate
90
90
  'weaviate',
91
+ // TigerBeetle
92
+ 'tigerbeetle',
91
93
  // Web panels
92
94
  'pgweb',
93
95
  // TUI tools
@@ -73,6 +73,7 @@ function getEngineDisplayName(engine: Engine): string {
73
73
  [Engine.TypeDB]: 'TypeDB',
74
74
  [Engine.InfluxDB]: 'InfluxDB',
75
75
  [Engine.Weaviate]: 'Weaviate',
76
+ [Engine.TigerBeetle]: 'TigerBeetle',
76
77
  }
77
78
  return displayNames[engine] || engine
78
79
  }
@@ -149,6 +150,9 @@ const _ENGINE_BINARY_CONFIG: Record<
149
150
  [Engine.Weaviate]: {
150
151
  primaryBinaries: [], // REST/GraphQL API only, no CLI tools
151
152
  },
153
+ [Engine.TigerBeetle]: {
154
+ primaryBinaries: ['tigerbeetle'],
155
+ },
152
156
  }
153
157
 
154
158
  /**
@@ -218,6 +222,9 @@ function getConnectionStringTemplate(
218
222
  case Engine.TypeDB:
219
223
  return `typedb://<host>:${port}`
220
224
 
225
+ case Engine.TigerBeetle:
226
+ return `<host>:${port}`
227
+
221
228
  case Engine.SQLite:
222
229
  case Engine.DuckDB:
223
230
  return `File-based database (no network connection)`
@@ -517,6 +524,13 @@ echo "No authentication required for local InfluxDB 3.x"
517
524
  userCreationCommands = `
518
525
  # TypeDB community edition does not support user management
519
526
  echo "No authentication required"
527
+ `
528
+ break
529
+
530
+ case Engine.TigerBeetle:
531
+ userCreationCommands = `
532
+ # TigerBeetle has no authentication
533
+ echo "No authentication required"
520
534
  `
521
535
  break
522
536
 
@@ -1315,6 +1329,9 @@ export async function getDockerConnectionString(
1315
1329
  case Engine.TypeDB:
1316
1330
  return `typedb://${host}:${port}`
1317
1331
 
1332
+ case Engine.TigerBeetle:
1333
+ return `${host}:${port}`
1334
+
1318
1335
  case Engine.SQLite:
1319
1336
  case Engine.DuckDB:
1320
1337
  return `File-based database (no network connection)`
@@ -58,8 +58,7 @@ export function detectLibraryError(
58
58
  lower.includes('dyld:') ||
59
59
  lower.includes('dyld[')
60
60
  ) {
61
- const needsOpenssl =
62
- lower.includes('libssl') || lower.includes('libcrypto')
61
+ const needsOpenssl = lower.includes('libssl') || lower.includes('libcrypto')
63
62
 
64
63
  if (needsOpenssl && plat === 'darwin') {
65
64
  return (
@@ -97,8 +96,7 @@ export function detectLibraryError(
97
96
  lower.includes('error while loading shared libraries') ||
98
97
  lower.includes('cannot open shared object file')
99
98
  ) {
100
- const needsOpenssl =
101
- lower.includes('libssl') || lower.includes('libcrypto')
99
+ const needsOpenssl = lower.includes('libssl') || lower.includes('libcrypto')
102
100
 
103
101
  if (needsOpenssl) {
104
102
  return (
@@ -2,6 +2,7 @@ import { exec } from 'child_process'
2
2
  import { promisify } from 'util'
3
3
  import { createRequire } from 'module'
4
4
  import { configManager } from './config-manager'
5
+ import { logDebug } from './error-handler'
5
6
 
6
7
  const execAsync = promisify(exec)
7
8
  const require = createRequire(import.meta.url)
@@ -11,6 +12,17 @@ const CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000 // 24 hours
11
12
 
12
13
  type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
13
14
 
15
+ const KNOWN_PACKAGE_MANAGERS: PackageManager[] = ['pnpm', 'yarn', 'bun', 'npm']
16
+
17
+ export function parseUserAgent(
18
+ userAgent: string | undefined,
19
+ ): PackageManager | null {
20
+ if (!userAgent) return null
21
+ const firstToken = userAgent.split('/')[0]?.toLowerCase().trim()
22
+ if (!firstToken) return null
23
+ return KNOWN_PACKAGE_MANAGERS.find((pm) => pm === firstToken) ?? null
24
+ }
25
+
14
26
  export type UpdateCheckResult = {
15
27
  currentVersion: string
16
28
  latestVersion: string
@@ -75,49 +87,59 @@ export class UpdateManager {
75
87
  }
76
88
  }
77
89
 
78
- // Checks pnpm, yarn, bun first since npm is the fallback
90
+ // Checks all PMs in parallel, falls back to npm_config_user_agent, then npm
79
91
  async detectPackageManager(): Promise<PackageManager> {
80
- try {
81
- const { stdout } = await execAsync('pnpm list -g spindb --json', {
82
- timeout: 5000,
83
- cwd: '/',
84
- })
85
- const data = JSON.parse(stdout) as Array<{
86
- dependencies?: { spindb?: unknown }
87
- }>
88
- if (data[0]?.dependencies?.spindb) {
89
- return 'pnpm'
90
- }
91
- } catch {
92
- // pnpm not installed or spindb not found
92
+ const checks = await Promise.all([
93
+ this.checkGlobalInstall(
94
+ 'pnpm',
95
+ 'pnpm list -g spindb --json',
96
+ (stdout) => {
97
+ const data = JSON.parse(stdout) as Array<{
98
+ dependencies?: { spindb?: unknown }
99
+ }>
100
+ return !!data[0]?.dependencies?.spindb
101
+ },
102
+ ),
103
+ this.checkGlobalInstall('yarn', 'yarn global list --json', (stdout) => {
104
+ return stdout.includes('"spindb@')
105
+ }),
106
+ this.checkGlobalInstall('bun', 'bun pm ls -g', (stdout) => {
107
+ return stdout.includes('spindb@')
108
+ }),
109
+ this.checkGlobalInstall('npm', 'npm list -g spindb --json', (stdout) => {
110
+ const data = JSON.parse(stdout) as {
111
+ dependencies?: { spindb?: unknown }
112
+ }
113
+ return !!data.dependencies?.spindb
114
+ }),
115
+ ])
116
+
117
+ const globalPm = checks.find((result) => result !== null)
118
+ if (globalPm) {
119
+ logDebug(`Detected global install via ${globalPm}`)
120
+ return globalPm
93
121
  }
94
122
 
95
- try {
96
- const { stdout } = await execAsync('yarn global list --json', {
97
- timeout: 5000,
98
- cwd: '/',
99
- })
100
- // yarn outputs newline-delimited JSON, look for spindb in any line
101
- if (stdout.includes('"spindb@')) {
102
- return 'yarn'
103
- }
104
- } catch {
105
- // yarn not installed or spindb not found
123
+ const agentPm = parseUserAgent(process.env.npm_config_user_agent)
124
+ if (agentPm) {
125
+ logDebug(`Detected package manager from user agent: ${agentPm}`)
126
+ return agentPm
106
127
  }
107
128
 
129
+ return 'npm'
130
+ }
131
+
132
+ private async checkGlobalInstall(
133
+ pm: PackageManager,
134
+ command: string,
135
+ checkOutput: (stdout: string) => boolean,
136
+ ): Promise<PackageManager | null> {
108
137
  try {
109
- const { stdout } = await execAsync('bun pm ls -g', {
110
- timeout: 5000,
111
- cwd: '/',
112
- })
113
- if (stdout.includes('spindb@')) {
114
- return 'bun'
115
- }
138
+ const { stdout } = await execAsync(command, { timeout: 5000, cwd: '/' })
139
+ return checkOutput(stdout) ? pm : null
116
140
  } catch {
117
- // bun not installed or spindb not found
141
+ return null
118
142
  }
119
-
120
- return 'npm'
121
143
  }
122
144
 
123
145
  getInstallCommand(pm: PackageManager): string {
@@ -169,6 +169,14 @@ export abstract class BaseEngine {
169
169
  throw new Error('influxdb3 not found')
170
170
  }
171
171
 
172
+ /**
173
+ * Get the path to the tigerbeetle binary if available
174
+ * Default implementation throws; TigerBeetle engine overrides this method.
175
+ */
176
+ async getTigerBeetlePath(_version?: string): Promise<string> {
177
+ throw new Error('tigerbeetle not found')
178
+ }
179
+
172
180
  /**
173
181
  * Get the path to the sqlite3 client if available
174
182
  * Default implementation returns null; SQLite engine overrides this method.
package/engines/index.ts CHANGED
@@ -17,6 +17,7 @@ import { questdbEngine } from './questdb'
17
17
  import { typedbEngine } from './typedb'
18
18
  import { influxdbEngine } from './influxdb'
19
19
  import { weaviateEngine } from './weaviate'
20
+ import { tigerbeetleEngine } from './tigerbeetle'
20
21
  import { platformService } from '../core/platform-service'
21
22
  import { Engine, Platform } from '../types'
22
23
  import type { BaseEngine } from './base-engine'
@@ -87,6 +88,9 @@ export const engines: Record<string, BaseEngine> = {
87
88
  // Weaviate and aliases
88
89
  [Engine.Weaviate]: weaviateEngine,
89
90
  wv: weaviateEngine,
91
+ // TigerBeetle and aliases
92
+ [Engine.TigerBeetle]: tigerbeetleEngine,
93
+ tb: tigerbeetleEngine,
90
94
  }
91
95
 
92
96
  // Get an engine by name
@@ -259,7 +259,10 @@ export class MariaDBEngine extends BaseEngine {
259
259
  return new Promise((resolve, reject) => {
260
260
  exec(
261
261
  cmd,
262
- { timeout: 120000, env: { ...process.env, ...getLibraryEnv(binPath) } },
262
+ {
263
+ timeout: 120000,
264
+ env: { ...process.env, ...getLibraryEnv(binPath) },
265
+ },
263
266
  async (error, stdout, stderr) => {
264
267
  if (error) {
265
268
  await cleanupOnFailure()
@@ -493,9 +496,7 @@ export class MariaDBEngine extends BaseEngine {
493
496
  }
494
497
 
495
498
  reject(
496
- new Error(
497
- libError || 'MariaDB failed to start within timeout',
498
- ),
499
+ new Error(libError || 'MariaDB failed to start within timeout'),
499
500
  )
500
501
  }
501
502
  }
@@ -594,6 +594,20 @@ export class RedisEngine extends BaseEngine {
594
594
  if (settled) return
595
595
 
596
596
  if (ready) {
597
+ // On Windows, Cygwin binaries may fork internally, making proc.pid stale.
598
+ // Find the actual PID by port and update the PID file (same pattern as QuestDB).
599
+ try {
600
+ const pids = await platformService.findProcessByPort(port)
601
+ if (pids.length > 0 && pids[0] !== proc.pid) {
602
+ logDebug(
603
+ `Redis actual PID ${pids[0]} differs from spawn PID ${proc.pid}, updating PID file`,
604
+ )
605
+ await writeFile(pidFile, String(pids[0]))
606
+ }
607
+ } catch {
608
+ // Non-fatal - PID file already has proc.pid from earlier write
609
+ }
610
+
597
611
  settled = true
598
612
  resolve({
599
613
  port,
@@ -698,10 +712,7 @@ export class RedisEngine extends BaseEngine {
698
712
  }
699
713
 
700
714
  // Check for library loading errors
701
- const libError = detectLibraryError(
702
- stderr + logContent,
703
- 'Redis',
704
- )
715
+ const libError = detectLibraryError(stderr + logContent, 'Redis')
705
716
  if (libError) {
706
717
  reject(new Error(libError))
707
718
  return
@@ -0,0 +1,61 @@
1
+ # TigerBeetle Engine
2
+
3
+ TigerBeetle is a high-performance financial ledger database written in Zig,
4
+ designed for mission-critical safety and performance.
5
+
6
+ ## Platform Support
7
+
8
+ All 5 platforms:
9
+ - darwin-arm64
10
+ - darwin-x64
11
+ - linux-arm64
12
+ - linux-x64
13
+ - win32-x64
14
+
15
+ ## Binary Structure
16
+
17
+ Single binary: `tigerbeetle` (handles both server and REPL client)
18
+
19
+ ## Two-Step Initialization
20
+
21
+ TigerBeetle requires a format step before starting:
22
+
23
+ 1. **Format**: `tigerbeetle format --cluster=0 --replica=0 --replica-count=1 --development <data-file>`
24
+ 2. **Start**: `tigerbeetle start --addresses=127.0.0.1:<port> --development <data-file>`
25
+
26
+ The `--development` flag is always passed since SpinDB is a local dev tool
27
+ (relaxes Direct I/O requirements).
28
+
29
+ ## REPL Usage
30
+
31
+ Connect to a running instance:
32
+ ```bash
33
+ tigerbeetle repl --cluster=0 --addresses=127.0.0.1:<port>
34
+ ```
35
+
36
+ ## Version Grouping
37
+
38
+ Uses xy-format: `0.16.70` groups as `0.16` (like MariaDB/ClickHouse).
39
+
40
+ ## Backup/Restore
41
+
42
+ Stop-and-copy of the single `0_0.tigerbeetle` data file.
43
+ The server must be stopped before backup (the file is exclusively locked).
44
+ TigerBeetle is designed for abrupt shutdown (SIGTERM/SIGKILL are safe).
45
+
46
+ ## Key Characteristics
47
+
48
+ - **Protocol**: Custom binary protocol (not REST, not SQL)
49
+ - **Auth**: None
50
+ - **Multi-database**: No (single ledger per instance)
51
+ - **Health check**: TCP port check + PID (no HTTP endpoint)
52
+ - **License**: Apache-2.0
53
+ - **Default port**: 3000
54
+
55
+ ## Linux / Docker Note
56
+
57
+ TigerBeetle uses `io_uring` for I/O on Linux. This works on regular Linux systems
58
+ (bare metal, VMs, GitHub Actions runners) but Docker's default seccomp profile blocks
59
+ `io_uring_*` syscalls. When running TigerBeetle inside Docker, the container must be
60
+ started with `--security-opt seccomp=unconfined`. The `pnpm test:docker` wrapper
61
+ handles this automatically. This does not affect macOS or Windows.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * TigerBeetle backup module
3
+ * Supports stop-and-copy backup of the single data file.
4
+ *
5
+ * TigerBeetle stores all data in a single file (e.g., 0_0.tigerbeetle).
6
+ * Backup requires the server to be stopped first since the data file
7
+ * is exclusively locked by the running process.
8
+ */
9
+
10
+ import { copyFile, mkdir, stat } from 'fs/promises'
11
+ import { existsSync } from 'fs'
12
+ import { dirname, join } from 'path'
13
+ import { logDebug } from '../../core/error-handler'
14
+ import type { BackupOptions, BackupResult } from '../../types'
15
+
16
+ /**
17
+ * Create a backup by copying the TigerBeetle data file.
18
+ * The server MUST be stopped before calling this function.
19
+ */
20
+ export async function createBackup(
21
+ dataDir: string,
22
+ outputPath: string,
23
+ _options: BackupOptions,
24
+ ): Promise<BackupResult> {
25
+ const dataFile = join(dataDir, '0_0.tigerbeetle')
26
+
27
+ if (!existsSync(dataFile)) {
28
+ throw new Error(
29
+ `TigerBeetle data file not found at ${dataFile}. Has the database been initialized?`,
30
+ )
31
+ }
32
+
33
+ // Ensure output parent directory exists
34
+ const outputDir = dirname(outputPath)
35
+ if (!existsSync(outputDir)) {
36
+ await mkdir(outputDir, { recursive: true })
37
+ }
38
+
39
+ logDebug(`Copying TigerBeetle data file to ${outputPath}`)
40
+ await copyFile(dataFile, outputPath)
41
+
42
+ const stats = await stat(outputPath)
43
+
44
+ return {
45
+ path: outputPath,
46
+ format: 'binary',
47
+ size: stats.size,
48
+ }
49
+ }