spindb 0.9.1 → 0.9.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.
Files changed (55) hide show
  1. package/README.md +5 -8
  2. package/cli/commands/attach.ts +108 -0
  3. package/cli/commands/backup.ts +13 -11
  4. package/cli/commands/clone.ts +14 -10
  5. package/cli/commands/config.ts +29 -29
  6. package/cli/commands/connect.ts +51 -39
  7. package/cli/commands/create.ts +65 -32
  8. package/cli/commands/delete.ts +8 -8
  9. package/cli/commands/deps.ts +17 -15
  10. package/cli/commands/detach.ts +100 -0
  11. package/cli/commands/doctor.ts +27 -13
  12. package/cli/commands/edit.ts +120 -57
  13. package/cli/commands/engines.ts +17 -15
  14. package/cli/commands/info.ts +8 -6
  15. package/cli/commands/list.ts +127 -18
  16. package/cli/commands/logs.ts +15 -11
  17. package/cli/commands/menu/backup-handlers.ts +52 -47
  18. package/cli/commands/menu/container-handlers.ts +164 -79
  19. package/cli/commands/menu/engine-handlers.ts +21 -11
  20. package/cli/commands/menu/index.ts +4 -4
  21. package/cli/commands/menu/shell-handlers.ts +34 -31
  22. package/cli/commands/menu/sql-handlers.ts +22 -16
  23. package/cli/commands/menu/update-handlers.ts +19 -17
  24. package/cli/commands/restore.ts +22 -20
  25. package/cli/commands/run.ts +20 -18
  26. package/cli/commands/self-update.ts +5 -5
  27. package/cli/commands/sqlite.ts +247 -0
  28. package/cli/commands/start.ts +11 -9
  29. package/cli/commands/stop.ts +9 -9
  30. package/cli/commands/url.ts +12 -9
  31. package/cli/helpers.ts +9 -4
  32. package/cli/index.ts +6 -0
  33. package/cli/ui/prompts.ts +12 -5
  34. package/cli/ui/spinner.ts +4 -4
  35. package/cli/ui/theme.ts +4 -4
  36. package/config/paths.ts +0 -8
  37. package/core/binary-manager.ts +5 -1
  38. package/core/config-manager.ts +32 -0
  39. package/core/container-manager.ts +5 -5
  40. package/core/platform-service.ts +3 -3
  41. package/core/start-with-retry.ts +6 -6
  42. package/core/transaction-manager.ts +6 -6
  43. package/engines/mysql/backup.ts +37 -13
  44. package/engines/mysql/index.ts +11 -11
  45. package/engines/mysql/restore.ts +4 -4
  46. package/engines/mysql/version-validator.ts +2 -2
  47. package/engines/postgresql/binary-manager.ts +17 -17
  48. package/engines/postgresql/index.ts +7 -2
  49. package/engines/postgresql/restore.ts +2 -2
  50. package/engines/postgresql/version-validator.ts +2 -2
  51. package/engines/sqlite/index.ts +30 -15
  52. package/engines/sqlite/registry.ts +64 -33
  53. package/engines/sqlite/scanner.ts +99 -0
  54. package/package.json +4 -3
  55. package/types/index.ts +21 -1
@@ -3,7 +3,7 @@ import { containerManager } from '../../core/container-manager'
3
3
  import { platformService } from '../../core/platform-service'
4
4
  import { getEngine } from '../../engines'
5
5
  import { promptContainerSelect } from '../ui/prompts'
6
- import { error, warning, success } from '../ui/theme'
6
+ import { uiError, uiWarning, uiSuccess } from '../ui/theme'
7
7
 
8
8
  export const urlCommand = new Command('url')
9
9
  .alias('connection-string')
@@ -24,7 +24,7 @@ export const urlCommand = new Command('url')
24
24
  const containers = await containerManager.list()
25
25
 
26
26
  if (containers.length === 0) {
27
- console.log(warning('No containers found'))
27
+ console.log(uiWarning('No containers found'))
28
28
  return
29
29
  }
30
30
 
@@ -38,13 +38,16 @@ export const urlCommand = new Command('url')
38
38
 
39
39
  const config = await containerManager.getConfig(containerName)
40
40
  if (!config) {
41
- console.error(error(`Container "${containerName}" not found`))
41
+ console.error(uiError(`Container "${containerName}" not found`))
42
42
  process.exit(1)
43
43
  }
44
44
 
45
45
  const engine = getEngine(config.engine)
46
46
  const databaseName = options.database || config.database
47
- const connectionString = engine.getConnectionString(config, databaseName)
47
+ const connectionString = engine.getConnectionString(
48
+ config,
49
+ databaseName,
50
+ )
48
51
 
49
52
  if (options.json) {
50
53
  const jsonOutput =
@@ -72,10 +75,10 @@ export const urlCommand = new Command('url')
72
75
  const copied = await platformService.copyToClipboard(connectionString)
73
76
  if (copied) {
74
77
  console.log(connectionString)
75
- console.error(success('Copied to clipboard'))
78
+ console.error(uiSuccess('Copied to clipboard'))
76
79
  } else {
77
80
  console.log(connectionString)
78
- console.error(warning('Could not copy to clipboard'))
81
+ console.error(uiWarning('Could not copy to clipboard'))
79
82
  }
80
83
  } else {
81
84
  process.stdout.write(connectionString)
@@ -83,9 +86,9 @@ export const urlCommand = new Command('url')
83
86
  console.log()
84
87
  }
85
88
  }
86
- } catch (err) {
87
- const e = err as Error
88
- console.error(error(e.message))
89
+ } catch (error) {
90
+ const e = error as Error
91
+ console.error(uiError(e.message))
89
92
  process.exit(1)
90
93
  }
91
94
  },
package/cli/helpers.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'fs'
2
2
  import { readdir, lstat } from 'fs/promises'
3
3
  import { join } from 'path'
4
- import { exec } from 'child_process'
4
+ import { exec, execFile } from 'child_process'
5
5
  import { promisify } from 'util'
6
6
  import { paths } from '../config/paths'
7
7
  import {
@@ -11,6 +11,7 @@ import {
11
11
  } from '../engines/mysql/binary-detection'
12
12
 
13
13
  const execAsync = promisify(exec)
14
+ const execFileAsync = promisify(execFile)
14
15
 
15
16
  export type InstalledPostgresEngine = {
16
17
  engine: 'postgresql'
@@ -49,7 +50,7 @@ async function getPostgresVersion(binPath: string): Promise<string | null> {
49
50
  }
50
51
 
51
52
  try {
52
- const { stdout } = await execAsync(`"${postgresPath}" --version`)
53
+ const { stdout } = await execFileAsync(postgresPath, ['--version'])
53
54
  const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
54
55
  return match ? match[1] : null
55
56
  } catch {
@@ -57,7 +58,9 @@ async function getPostgresVersion(binPath: string): Promise<string | null> {
57
58
  }
58
59
  }
59
60
 
60
- export async function getInstalledPostgresEngines(): Promise<InstalledPostgresEngine[]> {
61
+ export async function getInstalledPostgresEngines(): Promise<
62
+ InstalledPostgresEngine[]
63
+ > {
61
64
  const binDir = paths.bin
62
65
 
63
66
  if (!existsSync(binDir)) {
@@ -144,7 +147,9 @@ async function getInstalledSqliteEngine(): Promise<InstalledSqliteEngine | null>
144
147
  return null
145
148
  }
146
149
 
147
- const { stdout: versionOutput } = await execAsync(`"${sqlitePath}" --version`)
150
+ const { stdout: versionOutput } = await execFileAsync(sqlitePath, [
151
+ '--version',
152
+ ])
148
153
  // sqlite3 --version outputs: "3.43.2 2023-10-10 12:14:04 ..."
149
154
  const versionMatch = versionOutput.match(/^([\d.]+)/)
150
155
  const version = versionMatch ? versionMatch[1] : 'unknown'
package/cli/index.ts CHANGED
@@ -25,6 +25,9 @@ import { versionCommand } from './commands/version'
25
25
  import { runCommand } from './commands/run'
26
26
  import { logsCommand } from './commands/logs'
27
27
  import { doctorCommand } from './commands/doctor'
28
+ import { attachCommand } from './commands/attach'
29
+ import { detachCommand } from './commands/detach'
30
+ import { sqliteCommand } from './commands/sqlite'
28
31
  import { updateManager } from '../core/update-manager'
29
32
 
30
33
  /**
@@ -125,6 +128,9 @@ export async function run(): Promise<void> {
125
128
  program.addCommand(runCommand)
126
129
  program.addCommand(logsCommand)
127
130
  program.addCommand(doctorCommand)
131
+ program.addCommand(attachCommand)
132
+ program.addCommand(detachCommand)
133
+ program.addCommand(sqliteCommand)
128
134
 
129
135
  // If no arguments provided, show interactive menu
130
136
  if (process.argv.length <= 2) {
package/cli/ui/prompts.ts CHANGED
@@ -289,7 +289,9 @@ export async function promptDatabaseName(
289
289
  engine === 'mysql' ? 'Database (schema) name:' : 'Database name:'
290
290
 
291
291
  // Sanitize the default name to ensure it's valid
292
- const sanitizedDefault = defaultName ? sanitizeDatabaseName(defaultName) : undefined
292
+ const sanitizedDefault = defaultName
293
+ ? sanitizeDatabaseName(defaultName)
294
+ : undefined
293
295
 
294
296
  const { database } = await inquirer.prompt<{ database: string }>([
295
297
  {
@@ -420,7 +422,11 @@ export async function promptSqlitePath(
420
422
  ): Promise<string | undefined> {
421
423
  const defaultPath = `./${containerName}.sqlite`
422
424
 
423
- console.log(chalk.gray(' SQLite databases are stored as files in your project directory.'))
425
+ console.log(
426
+ chalk.gray(
427
+ ' SQLite databases are stored as files in your project directory.',
428
+ ),
429
+ )
424
430
  console.log(chalk.gray(` Default: ${defaultPath}`))
425
431
  console.log()
426
432
 
@@ -496,7 +502,8 @@ export async function promptSqlitePath(
496
502
  {
497
503
  type: 'list',
498
504
  name: 'overwrite',
499
- message: 'A file already exists at this location. What would you like to do?',
505
+ message:
506
+ 'A file already exists at this location. What would you like to do?',
500
507
  choices: [
501
508
  { name: 'Choose a different path', value: 'different' },
502
509
  { name: 'Cancel', value: 'cancel' },
@@ -702,8 +709,8 @@ export async function promptInstallDependencies(
702
709
 
703
710
  return false
704
711
  }
705
- } catch (err) {
706
- const e = err as Error
712
+ } catch (error) {
713
+ const e = error as Error
707
714
  console.log()
708
715
  console.log(chalk.red(` Installation failed: ${e.message}`))
709
716
  console.log()
package/cli/ui/spinner.ts CHANGED
@@ -27,10 +27,10 @@ export async function withSpinner<T>(
27
27
  })
28
28
  spinner.succeed()
29
29
  return result
30
- } catch (err) {
31
- const error = err as Error
32
- spinner.fail(error.message)
33
- throw error
30
+ } catch (error) {
31
+ const e = error as Error
32
+ spinner.fail(e.message)
33
+ throw e
34
34
  }
35
35
  }
36
36
 
package/cli/ui/theme.ts CHANGED
@@ -60,28 +60,28 @@ ${chalk.cyan('└' + line + '┘')}
60
60
  /**
61
61
  * Format a success message
62
62
  */
63
- export function success(message: string): string {
63
+ export function uiSuccess(message: string): string {
64
64
  return `${theme.icons.success} ${message}`
65
65
  }
66
66
 
67
67
  /**
68
68
  * Format an error message
69
69
  */
70
- export function error(message: string): string {
70
+ export function uiError(message: string): string {
71
71
  return `${theme.icons.error} ${chalk.red(message)}`
72
72
  }
73
73
 
74
74
  /**
75
75
  * Format a warning message
76
76
  */
77
- export function warning(message: string): string {
77
+ export function uiWarning(message: string): string {
78
78
  return `${theme.icons.warning} ${chalk.yellow(message)}`
79
79
  }
80
80
 
81
81
  /**
82
82
  * Format an info message
83
83
  */
84
- export function info(message: string): string {
84
+ export function uiInfo(message: string): string {
85
85
  return `${theme.icons.info} ${message}`
86
86
  }
87
87
 
package/config/paths.ts CHANGED
@@ -114,12 +114,4 @@ export const paths = {
114
114
  getEngineContainersPath(engine: string): string {
115
115
  return join(this.containers, engine)
116
116
  },
117
-
118
- /**
119
- * Get path for SQLite registry file
120
- * SQLite uses a registry (not container directories) since databases are stored externally
121
- */
122
- getSqliteRegistryPath(): string {
123
- return join(this.root, 'sqlite-registry.json')
124
- },
125
117
  }
@@ -6,7 +6,11 @@ import { exec } from 'child_process'
6
6
  import { promisify } from 'util'
7
7
  import { paths } from '../config/paths'
8
8
  import { defaults } from '../config/defaults'
9
- import { Engine, type ProgressCallback, type InstalledBinary } from '../types'
9
+ import {
10
+ type Engine,
11
+ type ProgressCallback,
12
+ type InstalledBinary,
13
+ } from '../types'
10
14
 
11
15
  const execAsync = promisify(exec)
12
16
 
@@ -10,6 +10,7 @@ import type {
10
10
  BinaryConfig,
11
11
  BinaryTool,
12
12
  BinarySource,
13
+ SQLiteEngineRegistry,
13
14
  } from '../types'
14
15
 
15
16
  const execAsync = promisify(exec)
@@ -349,6 +350,37 @@ export class ConfigManager {
349
350
  config.binaries = {}
350
351
  await this.save()
351
352
  }
353
+
354
+ // ============================================================
355
+ // SQLite Registry Methods
356
+ // ============================================================
357
+
358
+ /**
359
+ * Get the SQLite registry from config
360
+ * Returns empty registry if none exists
361
+ */
362
+ async getSqliteRegistry(): Promise<SQLiteEngineRegistry> {
363
+ const config = await this.load()
364
+ return (
365
+ config.registry?.sqlite ?? {
366
+ version: 1,
367
+ entries: [],
368
+ ignoreFolders: {},
369
+ }
370
+ )
371
+ }
372
+
373
+ /**
374
+ * Save the SQLite registry to config
375
+ */
376
+ async saveSqliteRegistry(registry: SQLiteEngineRegistry): Promise<void> {
377
+ const config = await this.load()
378
+ if (!config.registry) {
379
+ config.registry = {}
380
+ }
381
+ config.registry.sqlite = registry
382
+ await this.save()
383
+ }
352
384
  }
353
385
 
354
386
  export const configManager = new ConfigManager()
@@ -397,12 +397,12 @@ export class ContainerManager {
397
397
  await this.saveConfig(targetName, { engine }, config)
398
398
 
399
399
  return config
400
- } catch (err) {
400
+ } catch (error) {
401
401
  // Clean up the copied directory on failure
402
402
  await rm(targetPath, { recursive: true, force: true }).catch(() => {
403
403
  // Ignore cleanup errors
404
404
  })
405
- throw err
405
+ throw error
406
406
  }
407
407
  }
408
408
 
@@ -497,8 +497,8 @@ export class ContainerManager {
497
497
  try {
498
498
  // Try atomic rename first (only works on same filesystem)
499
499
  await fsRename(sourcePath, targetPath)
500
- } catch (err) {
501
- const e = err as NodeJS.ErrnoException
500
+ } catch (error) {
501
+ const e = error as NodeJS.ErrnoException
502
502
  if (e.code === 'EXDEV') {
503
503
  // Cross-filesystem move - fall back to copy+delete
504
504
  await cp(sourcePath, targetPath, { recursive: true })
@@ -514,7 +514,7 @@ export class ContainerManager {
514
514
  )
515
515
  }
516
516
  } else {
517
- throw err
517
+ throw error
518
518
  }
519
519
  }
520
520
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { homedir, platform as osPlatform, arch as osArch } from 'os'
14
- import { execSync, exec, spawn } from 'child_process'
14
+ import { execSync, execFileSync, exec, spawn } from 'child_process'
15
15
  import { promisify } from 'util'
16
16
  import { existsSync } from 'fs'
17
17
 
@@ -205,7 +205,7 @@ class DarwinPlatformService extends BasePlatformService {
205
205
  let getentResult: string | null = null
206
206
  if (sudoUser) {
207
207
  try {
208
- getentResult = execSync(`getent passwd ${sudoUser}`, {
208
+ getentResult = execFileSync('getent', ['passwd', sudoUser], {
209
209
  encoding: 'utf-8',
210
210
  })
211
211
  } catch {
@@ -347,7 +347,7 @@ class LinuxPlatformService extends BasePlatformService {
347
347
  let getentResult: string | null = null
348
348
  if (sudoUser) {
349
349
  try {
350
- getentResult = execSync(`getent passwd ${sudoUser}`, {
350
+ getentResult = execFileSync('getent', ['passwd', sudoUser], {
351
351
  encoding: 'utf-8',
352
352
  })
353
353
  } catch {
@@ -26,8 +26,8 @@ export type StartWithRetryResult = {
26
26
  error?: Error
27
27
  }
28
28
 
29
- function isPortInUseError(err: unknown): boolean {
30
- const message = (err as Error)?.message?.toLowerCase() || ''
29
+ function isPortInUseError(error: unknown): boolean {
30
+ const message = (error as Error)?.message?.toLowerCase() || ''
31
31
  return (
32
32
  message.includes('address already in use') ||
33
33
  message.includes('eaddrinuse') ||
@@ -62,14 +62,14 @@ export async function startWithRetry(
62
62
  finalPort: config.port,
63
63
  retriesUsed: attempt - 1,
64
64
  }
65
- } catch (err) {
66
- const isPortError = isPortInUseError(err)
65
+ } catch (error) {
66
+ const isPortError = isPortInUseError(error)
67
67
 
68
68
  logDebug(`Start attempt ${attempt} failed`, {
69
69
  containerName: config.name,
70
70
  port: config.port,
71
71
  isPortError,
72
- error: err instanceof Error ? err.message : String(err),
72
+ error: error instanceof Error ? error.message : String(error),
73
73
  })
74
74
 
75
75
  if (isPortError && attempt < maxRetries) {
@@ -101,7 +101,7 @@ export async function startWithRetry(
101
101
  success: false,
102
102
  finalPort: config.port,
103
103
  retriesUsed: attempt - 1,
104
- error: err instanceof Error ? err : new Error(String(err)),
104
+ error: error instanceof Error ? error : new Error(String(error)),
105
105
  }
106
106
  }
107
107
  }
@@ -36,9 +36,9 @@ export type RollbackAction = {
36
36
  * })
37
37
  *
38
38
  * tx.commit() // Success - clear rollback stack
39
- * } catch (err) {
39
+ * } catch (error) {
40
40
  * await tx.rollback() // Error - undo everything
41
- * throw err
41
+ * throw error
42
42
  * }
43
43
  * ```
44
44
  */
@@ -85,14 +85,14 @@ export class TransactionManager {
85
85
  logDebug(`Executing rollback: ${action.description}`)
86
86
  await action.execute()
87
87
  logDebug(`Rollback successful: ${action.description}`)
88
- } catch (err) {
88
+ } catch (error) {
89
89
  // Log error but continue with other rollbacks
90
90
  logError({
91
91
  code: ErrorCodes.ROLLBACK_FAILED,
92
92
  message: `Failed to rollback: ${action.description}`,
93
93
  severity: 'warning',
94
94
  context: {
95
- error: err instanceof Error ? err.message : String(err),
95
+ error: error instanceof Error ? error.message : String(error),
96
96
  },
97
97
  })
98
98
  }
@@ -155,8 +155,8 @@ export async function withTransaction<T>(
155
155
  const result = await operation(tx)
156
156
  tx.commit()
157
157
  return result
158
- } catch (err) {
158
+ } catch (error) {
159
159
  await tx.rollback()
160
- throw err
160
+ throw error
161
161
  }
162
162
  }
@@ -98,12 +98,20 @@ async function createSqlBackup(
98
98
 
99
99
  proc.on('close', async (code) => {
100
100
  if (code === 0) {
101
- const stats = await stat(outputPath)
102
- safeResolve({
103
- path: outputPath,
104
- format: 'sql',
105
- size: stats.size,
106
- })
101
+ try {
102
+ const stats = await stat(outputPath)
103
+ safeResolve({
104
+ path: outputPath,
105
+ format: 'sql',
106
+ size: stats.size,
107
+ })
108
+ } catch (error) {
109
+ safeReject(
110
+ new Error(
111
+ `Backup completed but failed to read output file: ${error instanceof Error ? error.message : String(error)}`,
112
+ ),
113
+ )
114
+ }
107
115
  } else {
108
116
  const errorMessage = stderr || `mysqldump exited with code ${code}`
109
117
  safeReject(new Error(errorMessage))
@@ -164,13 +172,29 @@ async function createCompressedBackup(
164
172
  })
165
173
  })
166
174
 
167
- // Wait for both pipeline AND process exit to succeed
168
- await Promise.all([pipelinePromise, exitPromise])
175
+ // Wait for both pipeline AND process exit to complete
176
+ // Use allSettled to handle case where both reject (avoids unhandled rejection)
177
+ const results = await Promise.allSettled([pipelinePromise, exitPromise])
169
178
 
170
- const stats = await stat(outputPath)
171
- return {
172
- path: outputPath,
173
- format: 'compressed',
174
- size: stats.size,
179
+ // Check for any rejections - prefer exitPromise error as it has more context
180
+ const [pipelineResult, exitResult] = results
181
+ if (exitResult.status === 'rejected') {
182
+ throw exitResult.reason
183
+ }
184
+ if (pipelineResult.status === 'rejected') {
185
+ throw pipelineResult.reason
186
+ }
187
+
188
+ try {
189
+ const stats = await stat(outputPath)
190
+ return {
191
+ path: outputPath,
192
+ format: 'compressed',
193
+ size: stats.size,
194
+ }
195
+ } catch (error) {
196
+ throw new Error(
197
+ `Backup completed but failed to read output file: ${error instanceof Error ? error.message : String(error)}`,
198
+ )
175
199
  }
176
200
  }
@@ -450,8 +450,8 @@ export class MySQLEngine extends BaseEngine {
450
450
  await this.cleanupPidFile(pidFile)
451
451
  return null
452
452
  }
453
- } catch (err) {
454
- const e = err as NodeJS.ErrnoException
453
+ } catch (error) {
454
+ const e = error as NodeJS.ErrnoException
455
455
  if (e.code !== 'ENOENT') {
456
456
  logWarning(`Failed to read PID file: ${e.message}`, {
457
457
  pidFile,
@@ -479,8 +479,8 @@ export class MySQLEngine extends BaseEngine {
479
479
  `"${mysqladmin}" -h 127.0.0.1 -P ${port} -u root shutdown`,
480
480
  { timeout: 5000 },
481
481
  )
482
- } catch (err) {
483
- const e = err as Error
482
+ } catch (error) {
483
+ const e = error as Error
484
484
  logDebug(`mysqladmin shutdown failed: ${e.message}`)
485
485
  // Continue to wait for process to die or send SIGTERM
486
486
  }
@@ -542,8 +542,8 @@ export class MySQLEngine extends BaseEngine {
542
542
  await this.cleanupPidFile(pidFile)
543
543
  return
544
544
  }
545
- } catch (err) {
546
- const e = err as NodeJS.ErrnoException
545
+ } catch (error) {
546
+ const e = error as NodeJS.ErrnoException
547
547
  if (e.code === 'ESRCH') {
548
548
  // Process already dead
549
549
  await this.cleanupPidFile(pidFile)
@@ -575,9 +575,9 @@ export class MySQLEngine extends BaseEngine {
575
575
  logDebug(`Process ${pid} terminated after SIGKILL`)
576
576
  await this.cleanupPidFile(pidFile)
577
577
  }
578
- } catch (err) {
579
- if (err instanceof SpinDBError) throw err
580
- const e = err as NodeJS.ErrnoException
578
+ } catch (error) {
579
+ if (error instanceof SpinDBError) throw error
580
+ const e = error as NodeJS.ErrnoException
581
581
  if (e.code === 'ESRCH') {
582
582
  // Process already dead
583
583
  await this.cleanupPidFile(pidFile)
@@ -594,8 +594,8 @@ export class MySQLEngine extends BaseEngine {
594
594
  try {
595
595
  await unlink(pidFile)
596
596
  logDebug('PID file cleaned up')
597
- } catch (err) {
598
- const e = err as NodeJS.ErrnoException
597
+ } catch (error) {
598
+ const e = error as NodeJS.ErrnoException
599
599
  if (e.code !== 'ENOENT') {
600
600
  logDebug(`Failed to clean up PID file: ${e.message}`)
601
601
  }
@@ -174,13 +174,13 @@ export async function restoreBackup(
174
174
  if (validateVersion) {
175
175
  try {
176
176
  await validateRestoreCompatibility({ dumpPath: backupPath })
177
- } catch (err) {
177
+ } catch (error) {
178
178
  // Re-throw SpinDBError, log and continue for other errors
179
- if (err instanceof Error && err.name === 'SpinDBError') {
180
- throw err
179
+ if (error instanceof Error && error.name === 'SpinDBError') {
180
+ throw error
181
181
  }
182
182
  logDebug('Version validation failed, proceeding anyway', {
183
- error: err instanceof Error ? err.message : String(err),
183
+ error: error instanceof Error ? error.message : String(error),
184
184
  })
185
185
  }
186
186
  }
@@ -197,10 +197,10 @@ export async function parseDumpVersion(dumpPath: string): Promise<DumpInfo> {
197
197
  }
198
198
 
199
199
  return { version: null, variant }
200
- } catch (err) {
200
+ } catch (error) {
201
201
  logDebug('Failed to parse dump version', {
202
202
  dumpPath,
203
- error: err instanceof Error ? err.message : String(err),
203
+ error: error instanceof Error ? error.message : String(error),
204
204
  })
205
205
  return { version: null, variant: 'unknown' }
206
206
  }