spindb 0.9.0 → 0.9.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 (49) hide show
  1. package/README.md +7 -0
  2. package/cli/commands/backup.ts +13 -11
  3. package/cli/commands/clone.ts +18 -8
  4. package/cli/commands/config.ts +29 -29
  5. package/cli/commands/connect.ts +51 -39
  6. package/cli/commands/create.ts +120 -43
  7. package/cli/commands/delete.ts +8 -8
  8. package/cli/commands/deps.ts +17 -15
  9. package/cli/commands/doctor.ts +16 -15
  10. package/cli/commands/edit.ts +115 -60
  11. package/cli/commands/engines.ts +50 -17
  12. package/cli/commands/info.ts +12 -8
  13. package/cli/commands/list.ts +34 -19
  14. package/cli/commands/logs.ts +24 -14
  15. package/cli/commands/menu/backup-handlers.ts +72 -49
  16. package/cli/commands/menu/container-handlers.ts +140 -80
  17. package/cli/commands/menu/engine-handlers.ts +145 -11
  18. package/cli/commands/menu/index.ts +4 -4
  19. package/cli/commands/menu/shell-handlers.ts +34 -31
  20. package/cli/commands/menu/sql-handlers.ts +22 -16
  21. package/cli/commands/menu/update-handlers.ts +19 -17
  22. package/cli/commands/restore.ts +105 -43
  23. package/cli/commands/run.ts +20 -18
  24. package/cli/commands/self-update.ts +5 -5
  25. package/cli/commands/start.ts +11 -9
  26. package/cli/commands/stop.ts +9 -9
  27. package/cli/commands/url.ts +12 -9
  28. package/cli/helpers.ts +49 -4
  29. package/cli/ui/prompts.ts +21 -8
  30. package/cli/ui/spinner.ts +4 -4
  31. package/cli/ui/theme.ts +4 -4
  32. package/core/binary-manager.ts +5 -1
  33. package/core/container-manager.ts +81 -30
  34. package/core/error-handler.ts +31 -0
  35. package/core/platform-service.ts +3 -3
  36. package/core/port-manager.ts +2 -0
  37. package/core/process-manager.ts +25 -3
  38. package/core/start-with-retry.ts +6 -6
  39. package/core/transaction-manager.ts +6 -6
  40. package/engines/mysql/backup.ts +53 -36
  41. package/engines/mysql/index.ts +59 -16
  42. package/engines/mysql/restore.ts +4 -4
  43. package/engines/mysql/version-validator.ts +2 -2
  44. package/engines/postgresql/binary-manager.ts +17 -17
  45. package/engines/postgresql/index.ts +13 -2
  46. package/engines/postgresql/restore.ts +2 -2
  47. package/engines/postgresql/version-validator.ts +2 -2
  48. package/engines/sqlite/index.ts +31 -9
  49. package/package.json +1 -1
@@ -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
  }
@@ -56,6 +56,20 @@ async function createSqlBackup(
56
56
  outputPath: string,
57
57
  ): Promise<BackupResult> {
58
58
  return new Promise((resolve, reject) => {
59
+ let settled = false
60
+ const safeResolve = (value: BackupResult) => {
61
+ if (!settled) {
62
+ settled = true
63
+ resolve(value)
64
+ }
65
+ }
66
+ const safeReject = (err: Error) => {
67
+ if (!settled) {
68
+ settled = true
69
+ reject(err)
70
+ }
71
+ }
72
+
59
73
  const args = [
60
74
  '-h',
61
75
  '127.0.0.1',
@@ -79,20 +93,20 @@ async function createSqlBackup(
79
93
  })
80
94
 
81
95
  proc.on('error', (err: NodeJS.ErrnoException) => {
82
- reject(err)
96
+ safeReject(err)
83
97
  })
84
98
 
85
99
  proc.on('close', async (code) => {
86
100
  if (code === 0) {
87
101
  const stats = await stat(outputPath)
88
- resolve({
102
+ safeResolve({
89
103
  path: outputPath,
90
104
  format: 'sql',
91
105
  size: stats.size,
92
106
  })
93
107
  } else {
94
108
  const errorMessage = stderr || `mysqldump exited with code ${code}`
95
- reject(new Error(errorMessage))
109
+ safeReject(new Error(errorMessage))
96
110
  }
97
111
  })
98
112
  })
@@ -108,52 +122,55 @@ async function createCompressedBackup(
108
122
  database: string,
109
123
  outputPath: string,
110
124
  ): Promise<BackupResult> {
111
- return new Promise((resolve, reject) => {
112
- const args = [
113
- '-h',
114
- '127.0.0.1',
115
- '-P',
116
- String(port),
117
- '-u',
118
- engineDef.superuser,
119
- database,
120
- ]
121
-
122
- const proc = spawn(mysqldump, args, {
123
- stdio: ['pipe', 'pipe', 'pipe'],
124
- })
125
+ const args = [
126
+ '-h',
127
+ '127.0.0.1',
128
+ '-P',
129
+ String(port),
130
+ '-u',
131
+ engineDef.superuser,
132
+ database,
133
+ ]
134
+
135
+ const proc = spawn(mysqldump, args, {
136
+ stdio: ['pipe', 'pipe', 'pipe'],
137
+ })
125
138
 
126
- const gzip = createGzip()
127
- const output = createWriteStream(outputPath)
139
+ const gzip = createGzip()
140
+ const output = createWriteStream(outputPath)
128
141
 
129
- let stderr = ''
142
+ let stderr = ''
130
143
 
131
- proc.stderr?.on('data', (data: Buffer) => {
132
- stderr += data.toString()
133
- })
144
+ proc.stderr?.on('data', (data: Buffer) => {
145
+ stderr += data.toString()
146
+ })
134
147
 
135
- // Pipe mysqldump stdout -> gzip -> file
136
- pipeline(proc.stdout!, gzip, output)
137
- .then(async () => {
138
- const stats = await stat(outputPath)
139
- resolve({
140
- path: outputPath,
141
- format: 'compressed',
142
- size: stats.size,
143
- })
144
- })
145
- .catch(reject)
148
+ // Create promise for pipeline completion
149
+ const pipelinePromise = pipeline(proc.stdout!, gzip, output)
146
150
 
151
+ // Create promise for process exit
152
+ const exitPromise = new Promise<void>((resolve, reject) => {
147
153
  proc.on('error', (err: NodeJS.ErrnoException) => {
148
154
  reject(err)
149
155
  })
150
156
 
151
157
  proc.on('close', (code) => {
152
- if (code !== 0) {
158
+ if (code === 0) {
159
+ resolve()
160
+ } else {
153
161
  const errorMessage = stderr || `mysqldump exited with code ${code}`
154
162
  reject(new Error(errorMessage))
155
163
  }
156
- // If code is 0, the pipeline promise will resolve
157
164
  })
158
165
  })
166
+
167
+ // Wait for both pipeline AND process exit to succeed
168
+ await Promise.all([pipelinePromise, exitPromise])
169
+
170
+ const stats = await stat(outputPath)
171
+ return {
172
+ path: outputPath,
173
+ format: 'compressed',
174
+ size: stats.size,
175
+ }
159
176
  }
@@ -6,7 +6,7 @@
6
6
  import { spawn, exec } from 'child_process'
7
7
  import { promisify } from 'util'
8
8
  import { existsSync, createReadStream } from 'fs'
9
- import { mkdir, writeFile, readFile, unlink } from 'fs/promises'
9
+ import { mkdir, writeFile, readFile, unlink, rm } from 'fs/promises'
10
10
  import { join } from 'path'
11
11
  import { BaseEngine } from '../base-engine'
12
12
  import { paths } from '../../config/paths'
@@ -16,6 +16,7 @@ import {
16
16
  logWarning,
17
17
  ErrorCodes,
18
18
  SpinDBError,
19
+ assertValidDatabaseName,
19
20
  } from '../../core/error-handler'
20
21
  import {
21
22
  getMysqldPath,
@@ -135,9 +136,27 @@ export class MySQLEngine extends BaseEngine {
135
136
  engine: ENGINE,
136
137
  })
137
138
 
139
+ // Track if we created the directory (for cleanup on failure)
140
+ let createdDataDir = false
141
+
138
142
  // Create data directory if it doesn't exist
139
143
  if (!existsSync(dataDir)) {
140
144
  await mkdir(dataDir, { recursive: true })
145
+ createdDataDir = true
146
+ }
147
+
148
+ // Helper to clean up on failure
149
+ const cleanupOnFailure = async () => {
150
+ if (createdDataDir) {
151
+ try {
152
+ await rm(dataDir, { recursive: true, force: true })
153
+ logDebug(`Cleaned up data directory after init failure: ${dataDir}`)
154
+ } catch (cleanupErr) {
155
+ logDebug(
156
+ `Failed to clean up data directory: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
157
+ )
158
+ }
159
+ }
141
160
  }
142
161
 
143
162
  // Check if we're using MariaDB or MySQL
@@ -148,6 +167,7 @@ export class MySQLEngine extends BaseEngine {
148
167
  const installDb =
149
168
  (await getMariadbInstallDbPath()) || (await getMysqlInstallDbPath())
150
169
  if (!installDb) {
170
+ await cleanupOnFailure()
151
171
  throw new Error(
152
172
  'MariaDB detected but mysql_install_db not found.\n' +
153
173
  'Install MariaDB server package which includes the initialization script.',
@@ -177,10 +197,11 @@ export class MySQLEngine extends BaseEngine {
177
197
  stderr += data.toString()
178
198
  })
179
199
 
180
- proc.on('close', (code) => {
200
+ proc.on('close', async (code) => {
181
201
  if (code === 0) {
182
202
  resolve(dataDir)
183
203
  } else {
204
+ await cleanupOnFailure()
184
205
  reject(
185
206
  new Error(
186
207
  `MariaDB initialization failed with code ${code}: ${stderr || stdout}`,
@@ -189,12 +210,16 @@ export class MySQLEngine extends BaseEngine {
189
210
  }
190
211
  })
191
212
 
192
- proc.on('error', reject)
213
+ proc.on('error', async (err) => {
214
+ await cleanupOnFailure()
215
+ reject(err)
216
+ })
193
217
  })
194
218
  } else {
195
219
  // MySQL uses mysqld --initialize-insecure
196
220
  const mysqld = await getMysqldPath()
197
221
  if (!mysqld) {
222
+ await cleanupOnFailure()
198
223
  throw new Error(getInstallInstructions())
199
224
  }
200
225
 
@@ -221,10 +246,11 @@ export class MySQLEngine extends BaseEngine {
221
246
  stderr += data.toString()
222
247
  })
223
248
 
224
- proc.on('close', (code) => {
249
+ proc.on('close', async (code) => {
225
250
  if (code === 0) {
226
251
  resolve(dataDir)
227
252
  } else {
253
+ await cleanupOnFailure()
228
254
  reject(
229
255
  new Error(
230
256
  `MySQL initialization failed with code ${code}: ${stderr || stdout}`,
@@ -233,7 +259,10 @@ export class MySQLEngine extends BaseEngine {
233
259
  }
234
260
  })
235
261
 
236
- proc.on('error', reject)
262
+ proc.on('error', async (err) => {
263
+ await cleanupOnFailure()
264
+ reject(err)
265
+ })
237
266
  })
238
267
  }
239
268
  }
@@ -313,6 +342,14 @@ export class MySQLEngine extends BaseEngine {
313
342
  connectionString: this.getConnectionString(container),
314
343
  })
315
344
  return
345
+ } else {
346
+ // mysqladmin not found - cannot verify MySQL is ready
347
+ reject(
348
+ new Error(
349
+ 'mysqladmin not found - cannot verify MySQL startup. Install MySQL client tools.',
350
+ ),
351
+ )
352
+ return
316
353
  }
317
354
  } catch {
318
355
  if (attempts < maxAttempts) {
@@ -413,8 +450,8 @@ export class MySQLEngine extends BaseEngine {
413
450
  await this.cleanupPidFile(pidFile)
414
451
  return null
415
452
  }
416
- } catch (err) {
417
- const e = err as NodeJS.ErrnoException
453
+ } catch (error) {
454
+ const e = error as NodeJS.ErrnoException
418
455
  if (e.code !== 'ENOENT') {
419
456
  logWarning(`Failed to read PID file: ${e.message}`, {
420
457
  pidFile,
@@ -442,8 +479,8 @@ export class MySQLEngine extends BaseEngine {
442
479
  `"${mysqladmin}" -h 127.0.0.1 -P ${port} -u root shutdown`,
443
480
  { timeout: 5000 },
444
481
  )
445
- } catch (err) {
446
- const e = err as Error
482
+ } catch (error) {
483
+ const e = error as Error
447
484
  logDebug(`mysqladmin shutdown failed: ${e.message}`)
448
485
  // Continue to wait for process to die or send SIGTERM
449
486
  }
@@ -505,8 +542,8 @@ export class MySQLEngine extends BaseEngine {
505
542
  await this.cleanupPidFile(pidFile)
506
543
  return
507
544
  }
508
- } catch (err) {
509
- const e = err as NodeJS.ErrnoException
545
+ } catch (error) {
546
+ const e = error as NodeJS.ErrnoException
510
547
  if (e.code === 'ESRCH') {
511
548
  // Process already dead
512
549
  await this.cleanupPidFile(pidFile)
@@ -538,9 +575,9 @@ export class MySQLEngine extends BaseEngine {
538
575
  logDebug(`Process ${pid} terminated after SIGKILL`)
539
576
  await this.cleanupPidFile(pidFile)
540
577
  }
541
- } catch (err) {
542
- if (err instanceof SpinDBError) throw err
543
- const e = err as NodeJS.ErrnoException
578
+ } catch (error) {
579
+ if (error instanceof SpinDBError) throw error
580
+ const e = error as NodeJS.ErrnoException
544
581
  if (e.code === 'ESRCH') {
545
582
  // Process already dead
546
583
  await this.cleanupPidFile(pidFile)
@@ -557,8 +594,8 @@ export class MySQLEngine extends BaseEngine {
557
594
  try {
558
595
  await unlink(pidFile)
559
596
  logDebug('PID file cleaned up')
560
- } catch (err) {
561
- const e = err as NodeJS.ErrnoException
597
+ } catch (error) {
598
+ const e = error as NodeJS.ErrnoException
562
599
  if (e.code !== 'ENOENT') {
563
600
  logDebug(`Failed to clean up PID file: ${e.message}`)
564
601
  }
@@ -687,6 +724,7 @@ export class MySQLEngine extends BaseEngine {
687
724
  container: ContainerConfig,
688
725
  database: string,
689
726
  ): Promise<void> {
727
+ assertValidDatabaseName(database)
690
728
  const { port } = container
691
729
 
692
730
  const mysql = await getMysqlClientPath()
@@ -720,6 +758,7 @@ export class MySQLEngine extends BaseEngine {
720
758
  container: ContainerConfig,
721
759
  database: string,
722
760
  ): Promise<void> {
761
+ assertValidDatabaseName(database)
723
762
  const { port } = container
724
763
 
725
764
  const mysql = await getMysqlClientPath()
@@ -748,6 +787,9 @@ export class MySQLEngine extends BaseEngine {
748
787
  const { port, database } = container
749
788
  const db = database || 'mysql'
750
789
 
790
+ // Validate database name to prevent SQL injection
791
+ assertValidDatabaseName(db)
792
+
751
793
  try {
752
794
  const mysql = await getMysqlClientPath()
753
795
  if (!mysql) return null
@@ -856,6 +898,7 @@ export class MySQLEngine extends BaseEngine {
856
898
  ): Promise<void> {
857
899
  const { port } = container
858
900
  const db = options.database || container.database || 'mysql'
901
+ assertValidDatabaseName(db)
859
902
 
860
903
  const mysql = await getMysqlClientPath()
861
904
  if (!mysql) {
@@ -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
  }
@@ -2,7 +2,7 @@ import { exec } from 'child_process'
2
2
  import { promisify } from 'util'
3
3
  import chalk from 'chalk'
4
4
  import { createSpinner } from '../../cli/ui/spinner'
5
- import { warning, error as themeError, success } from '../../cli/ui/theme'
5
+ import { uiWarning, uiError, uiSuccess } from '../../cli/ui/theme'
6
6
  import {
7
7
  detectPackageManager as detectPM,
8
8
  installEngineDependencies,
@@ -311,7 +311,7 @@ export async function installPostgresBinaries(): Promise<boolean> {
311
311
  const packageManager = await detectPM()
312
312
  if (!packageManager) {
313
313
  spinner.fail('No supported package manager found')
314
- console.log(themeError('Please install PostgreSQL client tools manually:'))
314
+ console.log(uiError('Please install PostgreSQL client tools manually:'))
315
315
 
316
316
  // Show platform-specific instructions from the registry
317
317
  const platform = getCurrentPlatform()
@@ -348,21 +348,21 @@ export async function installPostgresBinaries(): Promise<boolean> {
348
348
 
349
349
  if (allSuccess) {
350
350
  console.log()
351
- console.log(success('PostgreSQL client tools installed successfully'))
351
+ console.log(uiSuccess('PostgreSQL client tools installed successfully'))
352
352
  return true
353
353
  } else {
354
354
  const failed = results.filter((r) => !r.success)
355
355
  console.log()
356
- console.log(themeError('Some installations failed:'))
356
+ console.log(uiError('Some installations failed:'))
357
357
  for (const f of failed) {
358
- console.log(themeError(` ${f.dependency.name}: ${f.error}`))
358
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
359
359
  }
360
360
  return false
361
361
  }
362
362
  } catch (error: unknown) {
363
363
  console.log()
364
- console.log(themeError('Failed to install PostgreSQL client tools'))
365
- console.log(warning('Please install manually'))
364
+ console.log(uiError('Failed to install PostgreSQL client tools'))
365
+ console.log(uiWarning('Please install manually'))
366
366
  if (error instanceof Error) {
367
367
  console.log(chalk.gray(`Error details: ${error.message}`))
368
368
  }
@@ -408,7 +408,7 @@ export async function updatePostgresClientTools(): Promise<boolean> {
408
408
 
409
409
  spinner.succeed('PostgreSQL client tools updated')
410
410
  console.log(
411
- success(
411
+ uiSuccess(
412
412
  `Client tools successfully linked to PostgreSQL ${latestMajor}`,
413
413
  ),
414
414
  )
@@ -418,13 +418,13 @@ export async function updatePostgresClientTools(): Promise<boolean> {
418
418
  // For other package managers, use the standard update
419
419
  await execWithTimeout(packageManager.updateCommand('postgresql'), 120000)
420
420
  spinner.succeed('PostgreSQL client tools updated')
421
- console.log(success('Update completed successfully'))
421
+ console.log(uiSuccess('Update completed successfully'))
422
422
  return true
423
423
  }
424
424
  } catch (error: unknown) {
425
425
  spinner.fail('Update failed')
426
- console.log(themeError('Failed to update PostgreSQL client tools'))
427
- console.log(warning('Please update manually:'))
426
+ console.log(uiError('Failed to update PostgreSQL client tools'))
427
+ console.log(uiWarning('Please update manually:'))
428
428
 
429
429
  if (packageManager.name === 'brew') {
430
430
  const olderVersions = ['14', '15', '16'].filter((v) => v !== latestMajor)
@@ -473,12 +473,12 @@ export async function updatePostgresBinaries(): Promise<boolean> {
473
473
  try {
474
474
  await execWithTimeout(packageManager.updateCommand('postgresql'), 120000) // 2 minute timeout
475
475
  updateSpinner.succeed('PostgreSQL client tools updated')
476
- console.log(success('Update completed successfully'))
476
+ console.log(uiSuccess('Update completed successfully'))
477
477
  return true
478
478
  } catch (error: unknown) {
479
479
  updateSpinner.fail('Update failed')
480
- console.log(themeError('Failed to update PostgreSQL client tools'))
481
- console.log(warning('Please update manually:'))
480
+ console.log(uiError('Failed to update PostgreSQL client tools'))
481
+ console.log(uiWarning('Please update manually:'))
482
482
  console.log(` ${packageManager.updateCommand('postgresql')}`)
483
483
  if (error instanceof Error) {
484
484
  console.log(chalk.gray(`Error details: ${error.message}`))
@@ -505,7 +505,7 @@ export async function ensurePostgresBinary(
505
505
  return { success: false, info: null, action: 'install_required' }
506
506
  }
507
507
 
508
- console.log(warning(`${binary} not found on your system`))
508
+ console.log(uiWarning(`${binary} not found on your system`))
509
509
  const success = await installPostgresBinaries()
510
510
  if (!success) {
511
511
  return { success: false, info: null, action: 'install_failed' }
@@ -527,13 +527,13 @@ export async function ensurePostgresBinary(
527
527
  }
528
528
 
529
529
  console.log(
530
- warning(
530
+ uiWarning(
531
531
  `Your ${binary} version (${info.version}) is incompatible with the dump file`,
532
532
  ),
533
533
  )
534
534
  if (info.requiredVersion) {
535
535
  console.log(
536
- warning(`Required version: ${info.requiredVersion} or compatible`),
536
+ uiWarning(`Required version: ${info.requiredVersion} or compatible`),
537
537
  )
538
538
  }
539
539
 
@@ -18,6 +18,7 @@ import {
18
18
  } from './binary-urls'
19
19
  import { detectBackupFormat, restoreBackup } from './restore'
20
20
  import { createBackup } from './backup'
21
+ import { assertValidDatabaseName } from '../../core/error-handler'
21
22
  import type {
22
23
  ContainerConfig,
23
24
  ProgressCallback,
@@ -154,8 +155,13 @@ export class PostgreSQLEngine extends BaseEngine {
154
155
 
155
156
  // Configure max_connections after initdb creates postgresql.conf
156
157
  const maxConnections =
157
- (options.maxConnections as number) || getEngineDefaults('postgresql').maxConnections
158
- await this.setConfigValue(dataDir, 'max_connections', String(maxConnections))
158
+ (options.maxConnections as number) ||
159
+ getEngineDefaults('postgresql').maxConnections
160
+ await this.setConfigValue(
161
+ dataDir,
162
+ 'max_connections',
163
+ String(maxConnections),
164
+ )
159
165
 
160
166
  return dataDir
161
167
  }
@@ -395,6 +401,7 @@ export class PostgreSQLEngine extends BaseEngine {
395
401
  container: ContainerConfig,
396
402
  database: string,
397
403
  ): Promise<void> {
404
+ assertValidDatabaseName(database)
398
405
  const { port } = container
399
406
  const psqlPath = await this.getPsqlPath()
400
407
 
@@ -418,6 +425,7 @@ export class PostgreSQLEngine extends BaseEngine {
418
425
  container: ContainerConfig,
419
426
  database: string,
420
427
  ): Promise<void> {
428
+ assertValidDatabaseName(database)
421
429
  const { port } = container
422
430
  const psqlPath = await this.getPsqlPath()
423
431
 
@@ -443,6 +451,9 @@ export class PostgreSQLEngine extends BaseEngine {
443
451
  const { port, database } = container
444
452
  const db = database || 'postgres'
445
453
 
454
+ // Validate database name to prevent SQL injection
455
+ assertValidDatabaseName(db)
456
+
446
457
  try {
447
458
  const psqlPath = await this.getPsqlPath()
448
459
  // Query pg_database_size for the specific database
@@ -219,8 +219,8 @@ export async function restoreBackup(
219
219
  format: detectedFormat,
220
220
  ...result,
221
221
  }
222
- } catch (err) {
223
- const e = err as Error & { stdout?: string; stderr?: string }
222
+ } catch (error) {
223
+ const e = error as Error & { stdout?: string; stderr?: string }
224
224
  // pg_restore often returns non-zero even on partial success
225
225
  return {
226
226
  format: detectedFormat,
@@ -135,11 +135,11 @@ export async function parseDumpVersion(
135
135
  }
136
136
  }
137
137
  }
138
- } catch (err) {
138
+ } catch (error) {
139
139
  logDebug('Failed to parse dump version', {
140
140
  dumpPath,
141
141
  format,
142
- error: err instanceof Error ? err.message : String(err),
142
+ error: error instanceof Error ? error.message : String(error),
143
143
  })
144
144
  }
145
145