spindb 0.36.2 → 0.37.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 (38) hide show
  1. package/README.md +19 -8
  2. package/cli/commands/create.ts +7 -0
  3. package/cli/commands/databases.ts +17 -12
  4. package/cli/commands/delete.ts +3 -0
  5. package/cli/commands/engines.ts +59 -3
  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/restore.ts +3 -0
  12. package/cli/commands/start.ts +3 -0
  13. package/cli/commands/url.ts +4 -0
  14. package/cli/constants.ts +4 -0
  15. package/cli/helpers.ts +93 -0
  16. package/config/backup-formats.ts +14 -0
  17. package/config/engine-defaults.ts +13 -0
  18. package/config/engines.json +17 -0
  19. package/core/config-manager.ts +5 -0
  20. package/core/dependency-manager.ts +2 -0
  21. package/core/docker-exporter.ts +17 -0
  22. package/core/library-env.ts +2 -4
  23. package/engines/base-engine.ts +8 -0
  24. package/engines/index.ts +4 -0
  25. package/engines/mariadb/index.ts +5 -4
  26. package/engines/redis/index.ts +15 -4
  27. package/engines/tigerbeetle/README.md +61 -0
  28. package/engines/tigerbeetle/backup.ts +49 -0
  29. package/engines/tigerbeetle/binary-manager.ts +95 -0
  30. package/engines/tigerbeetle/binary-urls.ts +62 -0
  31. package/engines/tigerbeetle/hostdb-releases.ts +26 -0
  32. package/engines/tigerbeetle/index.ts +746 -0
  33. package/engines/tigerbeetle/restore.ts +130 -0
  34. package/engines/tigerbeetle/version-maps.ts +68 -0
  35. package/engines/tigerbeetle/version-validator.ts +126 -0
  36. package/engines/valkey/index.ts +15 -4
  37. package/package.json +2 -1
  38. package/types/index.ts +9 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * TigerBeetle restore module
3
+ * Supports restoring from a TigerBeetle data file backup.
4
+ *
5
+ * Restore copies the backup data file into the container's data directory.
6
+ * The server must be stopped before restore.
7
+ */
8
+
9
+ import { copyFile, mkdir, stat } from 'fs/promises'
10
+ import { existsSync } from 'fs'
11
+ import { join } from 'path'
12
+ import { logDebug } from '../../core/error-handler'
13
+ import type { BackupFormat, RestoreResult } from '../../types'
14
+
15
+ /**
16
+ * Detect backup format from a file path.
17
+ * TigerBeetle backups are single binary data files.
18
+ */
19
+ export async function detectBackupFormat(
20
+ filePath: string,
21
+ ): Promise<BackupFormat> {
22
+ if (!existsSync(filePath)) {
23
+ throw new Error(`Backup file not found: ${filePath}`)
24
+ }
25
+
26
+ const stats = await stat(filePath)
27
+
28
+ // TigerBeetle data files are regular files (not directories)
29
+ if (stats.isFile()) {
30
+ if (filePath.endsWith('.tigerbeetle')) {
31
+ return {
32
+ format: 'binary',
33
+ description: 'TigerBeetle data file',
34
+ restoreCommand: 'Copy to data directory (spindb restore handles this)',
35
+ }
36
+ }
37
+
38
+ // Check for common backup naming patterns
39
+ return {
40
+ format: 'binary',
41
+ description: 'TigerBeetle data file (assumed from file)',
42
+ restoreCommand: 'Copy to data directory (spindb restore handles this)',
43
+ }
44
+ }
45
+
46
+ return {
47
+ format: 'unknown',
48
+ description: 'Unknown backup format',
49
+ restoreCommand: 'Use a TigerBeetle data file (.tigerbeetle) for restore',
50
+ }
51
+ }
52
+
53
+ // Restore options for TigerBeetle
54
+ export type RestoreOptions = {
55
+ containerName: string
56
+ dataDir: string
57
+ }
58
+
59
+ /**
60
+ * Restore from a TigerBeetle data file backup.
61
+ * Copies the backup file into the container's data directory.
62
+ */
63
+ export async function restoreBackup(
64
+ backupPath: string,
65
+ options: RestoreOptions,
66
+ ): Promise<RestoreResult> {
67
+ const { dataDir } = options
68
+
69
+ if (!existsSync(backupPath)) {
70
+ throw new Error(`Backup not found: ${backupPath}`)
71
+ }
72
+
73
+ const format = await detectBackupFormat(backupPath)
74
+ logDebug(`Detected backup format: ${format.format}`)
75
+
76
+ // Ensure data directory exists
77
+ if (!existsSync(dataDir)) {
78
+ await mkdir(dataDir, { recursive: true })
79
+ }
80
+
81
+ const targetPath = join(dataDir, '0_0.tigerbeetle')
82
+
83
+ logDebug(`Restoring TigerBeetle data file to: ${targetPath}`)
84
+ await copyFile(backupPath, targetPath)
85
+
86
+ return {
87
+ format: 'binary',
88
+ stdout: `Restored TigerBeetle data file to ${targetPath}`,
89
+ code: 0,
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Parse TigerBeetle connection string
95
+ * Format: host:port or 127.0.0.1:port
96
+ *
97
+ * TigerBeetle uses a custom binary protocol (no URI scheme).
98
+ */
99
+ export function parseConnectionString(connectionString: string): {
100
+ host: string
101
+ port: number
102
+ } {
103
+ if (!connectionString || typeof connectionString !== 'string') {
104
+ throw new Error(
105
+ 'Invalid TigerBeetle connection string: expected a non-empty string',
106
+ )
107
+ }
108
+
109
+ const trimmed = connectionString.trim()
110
+
111
+ // Parse host:port format
112
+ const parts = trimmed.split(':')
113
+ if (parts.length !== 2) {
114
+ throw new Error(
115
+ `Invalid TigerBeetle connection string: "${trimmed}". ` +
116
+ 'Expected format: host:port (e.g., 127.0.0.1:3000)',
117
+ )
118
+ }
119
+
120
+ const host = parts[0] || '127.0.0.1'
121
+ const port = parseInt(parts[1], 10)
122
+
123
+ if (isNaN(port) || port <= 0 || port > 65535) {
124
+ throw new Error(
125
+ `Invalid TigerBeetle port: "${parts[1]}". Expected a number between 1 and 65535.`,
126
+ )
127
+ }
128
+
129
+ return { host, port }
130
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * TigerBeetle Version Maps
3
+ *
4
+ * TEMPORARY: This version map will be replaced by the hostdb npm package once published.
5
+ * Until then, manually keep this in sync with robertjbass/hostdb releases.json:
6
+ * https://github.com/robertjbass/hostdb/blob/main/releases.json
7
+ *
8
+ * When updating versions:
9
+ * 1. Check hostdb releases.json for available versions
10
+ * 2. Update TIGERBEETLE_VERSION_MAP to match
11
+ */
12
+
13
+ import { logDebug } from '../../core/error-handler'
14
+
15
+ /**
16
+ * Map of major TigerBeetle versions to their latest stable patch versions.
17
+ * Must match versions available in hostdb releases.json.
18
+ *
19
+ * TigerBeetle uses xy-format grouping (like MariaDB/ClickHouse):
20
+ * 0.16.70 groups as "0.16"
21
+ */
22
+ export const TIGERBEETLE_VERSION_MAP: Record<string, string> = {
23
+ // 1-part: major version → latest
24
+ '0': '0.16.70',
25
+ // 2-part: major.minor → latest patch
26
+ '0.16': '0.16.70',
27
+ // 3-part: exact version (identity mapping)
28
+ '0.16.70': '0.16.70',
29
+ }
30
+
31
+ /**
32
+ * Supported major TigerBeetle versions (2-part format).
33
+ * Derived from TIGERBEETLE_VERSION_MAP keys to avoid duplication.
34
+ * Used for grouping and display purposes.
35
+ */
36
+ export const SUPPORTED_MAJOR_VERSIONS = Object.keys(
37
+ TIGERBEETLE_VERSION_MAP,
38
+ ).filter((key) => key.split('.').length === 2)
39
+
40
+ /**
41
+ * Get the full version string for a major version.
42
+ *
43
+ * @param majorVersion - Major version (e.g., '0.16')
44
+ * @returns Full version string (e.g., '0.16.70') or null if not supported
45
+ */
46
+ export function getFullVersion(majorVersion: string): string | null {
47
+ return TIGERBEETLE_VERSION_MAP[majorVersion] || null
48
+ }
49
+
50
+ /**
51
+ * Normalize a version string to X.Y.Z format.
52
+ *
53
+ * @param version - Version string (e.g., '0', '0.16', '0.16.70')
54
+ * @returns Normalized version (e.g., '0.16.70')
55
+ */
56
+ export function normalizeVersion(version: string): string {
57
+ // If it's a version key in the map (major, major.minor, or full), return the mapped version
58
+ const fullVersion = TIGERBEETLE_VERSION_MAP[version]
59
+ if (fullVersion) {
60
+ return fullVersion
61
+ }
62
+
63
+ // Unknown version - log debug and return as-is
64
+ logDebug(
65
+ `TigerBeetle version '${version}' not in version map, may not be available in hostdb`,
66
+ )
67
+ return version
68
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * TigerBeetle version validation utilities
3
+ * Handles version parsing, comparison, and compatibility checking
4
+ */
5
+
6
+ /**
7
+ * Parse a TigerBeetle version string into components
8
+ * Handles formats like "0.16.70", "0.16", "v0.16.70"
9
+ */
10
+ export function parseVersion(versionString: string): {
11
+ major: number
12
+ minor: number
13
+ patch: number
14
+ raw: string
15
+ } | null {
16
+ const cleaned = versionString.replace(/^v/, '').trim()
17
+ if (!cleaned) return null
18
+
19
+ const parts = cleaned.split('.')
20
+
21
+ const major = parseInt(parts[0], 10)
22
+ const minor = parts[1] ? parseInt(parts[1], 10) : 0
23
+ const patch = parts[2] ? parseInt(parts[2], 10) : 0
24
+
25
+ if (isNaN(major)) return null
26
+ if (parts[1] && isNaN(minor)) return null
27
+ if (parts[2] && isNaN(patch)) return null
28
+
29
+ return { major, minor, patch, raw: cleaned }
30
+ }
31
+
32
+ /**
33
+ * Check if a TigerBeetle version is supported by SpinDB
34
+ * Minimum supported version: 0.16.0
35
+ */
36
+ export function isVersionSupported(version: string): boolean {
37
+ const parsed = parseVersion(version)
38
+ if (!parsed) return false
39
+
40
+ // Support 0.16+
41
+ if (parsed.major === 0) {
42
+ return parsed.minor >= 16
43
+ }
44
+ return parsed.major >= 1
45
+ }
46
+
47
+ /**
48
+ * Get major version from full version string (xy-format: 2-part)
49
+ * e.g., "0.16.70" -> "0.16"
50
+ */
51
+ export function getMajorVersion(version: string): string {
52
+ const parsed = parseVersion(version)
53
+ if (!parsed) return version
54
+ return `${parsed.major}.${parsed.minor}`
55
+ }
56
+
57
+ /**
58
+ * Get major.minor version from full version string.
59
+ * Intentional alias for getMajorVersion — both return the 2-part xy-format
60
+ * version (e.g., "0.16"). Kept as a separate export for API consistency
61
+ * with other engine version validators.
62
+ */
63
+ export function getMajorMinorVersion(version: string): string {
64
+ return getMajorVersion(version)
65
+ }
66
+
67
+ /**
68
+ * Compare two TigerBeetle versions
69
+ * Returns: -1 if a < b, 0 if a == b, 1 if a > b, null if either version cannot be parsed
70
+ */
71
+ export function compareVersions(a: string, b: string): number | null {
72
+ const parsedA = parseVersion(a)
73
+ const parsedB = parseVersion(b)
74
+
75
+ if (!parsedA || !parsedB) {
76
+ return null
77
+ }
78
+
79
+ if (parsedA.major !== parsedB.major) {
80
+ return parsedA.major < parsedB.major ? -1 : 1
81
+ }
82
+ if (parsedA.minor !== parsedB.minor) {
83
+ return parsedA.minor < parsedB.minor ? -1 : 1
84
+ }
85
+ if (parsedA.patch !== parsedB.patch) {
86
+ return parsedA.patch < parsedB.patch ? -1 : 1
87
+ }
88
+ return 0
89
+ }
90
+
91
+ /**
92
+ * Check if a backup version is compatible with the restore version
93
+ * TigerBeetle data files are generally compatible within minor versions
94
+ */
95
+ export function isVersionCompatible(
96
+ backupVersion: string,
97
+ restoreVersion: string,
98
+ ): { compatible: boolean; warning?: string } {
99
+ const backup = parseVersion(backupVersion)
100
+ const restore = parseVersion(restoreVersion)
101
+
102
+ if (!backup || !restore) {
103
+ return {
104
+ compatible: true,
105
+ warning: 'Could not parse versions, proceeding with restore',
106
+ }
107
+ }
108
+
109
+ // Must be same major.minor for TigerBeetle
110
+ if (backup.major !== restore.major || backup.minor !== restore.minor) {
111
+ return {
112
+ compatible: false,
113
+ warning: `Cannot restore TigerBeetle ${backupVersion} data to ${restoreVersion} server. Major.minor versions must match.`,
114
+ }
115
+ }
116
+
117
+ return { compatible: true }
118
+ }
119
+
120
+ /**
121
+ * Validate that a version string matches supported format
122
+ */
123
+ export function isValidVersionFormat(version: string): boolean {
124
+ const parsed = parseVersion(version)
125
+ return parsed !== null
126
+ }
@@ -601,6 +601,20 @@ export class ValkeyEngine extends BaseEngine {
601
601
  if (settled) return
602
602
 
603
603
  if (ready) {
604
+ // On Windows, Cygwin binaries may fork internally, making proc.pid stale.
605
+ // Find the actual PID by port and update the PID file (same pattern as QuestDB).
606
+ try {
607
+ const pids = await platformService.findProcessByPort(port)
608
+ if (pids.length > 0 && pids[0] !== proc.pid) {
609
+ logDebug(
610
+ `Valkey actual PID ${pids[0]} differs from spawn PID ${proc.pid}, updating PID file`,
611
+ )
612
+ await writeFile(pidFile, String(pids[0]))
613
+ }
614
+ } catch {
615
+ // Non-fatal - PID file already has proc.pid from earlier write
616
+ }
617
+
604
618
  settled = true
605
619
  resolve({
606
620
  port,
@@ -703,10 +717,7 @@ export class ValkeyEngine extends BaseEngine {
703
717
  } catch {
704
718
  logContent = ''
705
719
  }
706
- const libError = detectLibraryError(
707
- stderr + logContent,
708
- 'Valkey',
709
- )
720
+ const libError = detectLibraryError(stderr + logContent, 'Valkey')
710
721
  if (libError) {
711
722
  reject(new Error(libError))
712
723
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.36.2",
3
+ "version": "0.37.1",
4
4
  "author": "Bob Bass <bob@bbass.co>",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
@@ -57,6 +57,7 @@
57
57
  "typedb",
58
58
  "influxdb",
59
59
  "weaviate",
60
+ "tigerbeetle",
60
61
  "layerbase",
61
62
  "docker-free",
62
63
  "tui",
package/types/index.ts CHANGED
@@ -40,6 +40,7 @@ export enum Engine {
40
40
  TypeDB = 'typedb',
41
41
  InfluxDB = 'influxdb',
42
42
  Weaviate = 'weaviate',
43
+ TigerBeetle = 'tigerbeetle',
43
44
  }
44
45
 
45
46
  // Icon display mode for engine icons in CLI output
@@ -82,6 +83,7 @@ export const ALL_ENGINES = [
82
83
  Engine.TypeDB,
83
84
  Engine.InfluxDB,
84
85
  Engine.Weaviate,
86
+ Engine.TigerBeetle,
85
87
  ] as const
86
88
 
87
89
  // File-based engines (no server process, data stored in user project directories)
@@ -214,6 +216,7 @@ export type QuestDBFormat = 'sql'
214
216
  export type TypeDBFormat = 'typeql'
215
217
  export type InfluxDBFormat = 'sql'
216
218
  export type WeaviateFormat = 'snapshot'
219
+ export type TigerBeetleFormat = 'binary'
217
220
 
218
221
  // Query command types
219
222
  export type QueryResultRow = Record<string, unknown>
@@ -294,6 +297,7 @@ export type BackupFormatType =
294
297
  | TypeDBFormat
295
298
  | InfluxDBFormat
296
299
  | WeaviateFormat
300
+ | TigerBeetleFormat
297
301
 
298
302
  // Mapping from Engine to its corresponding backup format type
299
303
  type EngineFormatMap = {
@@ -316,6 +320,7 @@ type EngineFormatMap = {
316
320
  [Engine.TypeDB]: TypeDBFormat
317
321
  [Engine.InfluxDB]: InfluxDBFormat
318
322
  [Engine.Weaviate]: WeaviateFormat
323
+ [Engine.TigerBeetle]: TigerBeetleFormat
319
324
  }
320
325
 
321
326
  // Helper type to get format type for a specific engine
@@ -434,6 +439,8 @@ export type BinaryTool =
434
439
  | 'influxdb3'
435
440
  // Weaviate tools
436
441
  | 'weaviate'
442
+ // TigerBeetle tools
443
+ | 'tigerbeetle'
437
444
  // Web panels
438
445
  | 'pgweb'
439
446
  // TUI tools
@@ -532,6 +539,8 @@ export type SpinDBConfig = {
532
539
  influxdb3?: BinaryConfig
533
540
  // Weaviate tools
534
541
  weaviate?: BinaryConfig
542
+ // TigerBeetle tools
543
+ tigerbeetle?: BinaryConfig
535
544
  // Web panels
536
545
  pgweb?: BinaryConfig
537
546
  // TUI tools