spindb 0.9.1 → 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 (44) hide show
  1. package/cli/commands/backup.ts +13 -11
  2. package/cli/commands/clone.ts +14 -10
  3. package/cli/commands/config.ts +29 -29
  4. package/cli/commands/connect.ts +51 -39
  5. package/cli/commands/create.ts +59 -32
  6. package/cli/commands/delete.ts +8 -8
  7. package/cli/commands/deps.ts +17 -15
  8. package/cli/commands/doctor.ts +11 -11
  9. package/cli/commands/edit.ts +108 -55
  10. package/cli/commands/engines.ts +17 -15
  11. package/cli/commands/info.ts +8 -6
  12. package/cli/commands/list.ts +31 -16
  13. package/cli/commands/logs.ts +15 -11
  14. package/cli/commands/menu/backup-handlers.ts +52 -47
  15. package/cli/commands/menu/container-handlers.ts +120 -78
  16. package/cli/commands/menu/engine-handlers.ts +21 -11
  17. package/cli/commands/menu/index.ts +4 -4
  18. package/cli/commands/menu/shell-handlers.ts +34 -31
  19. package/cli/commands/menu/sql-handlers.ts +22 -16
  20. package/cli/commands/menu/update-handlers.ts +19 -17
  21. package/cli/commands/restore.ts +22 -20
  22. package/cli/commands/run.ts +20 -18
  23. package/cli/commands/self-update.ts +5 -5
  24. package/cli/commands/start.ts +11 -9
  25. package/cli/commands/stop.ts +9 -9
  26. package/cli/commands/url.ts +12 -9
  27. package/cli/helpers.ts +9 -4
  28. package/cli/ui/prompts.ts +12 -5
  29. package/cli/ui/spinner.ts +4 -4
  30. package/cli/ui/theme.ts +4 -4
  31. package/core/binary-manager.ts +5 -1
  32. package/core/container-manager.ts +5 -5
  33. package/core/platform-service.ts +3 -3
  34. package/core/start-with-retry.ts +6 -6
  35. package/core/transaction-manager.ts +6 -6
  36. package/engines/mysql/index.ts +11 -11
  37. package/engines/mysql/restore.ts +4 -4
  38. package/engines/mysql/version-validator.ts +2 -2
  39. package/engines/postgresql/binary-manager.ts +17 -17
  40. package/engines/postgresql/index.ts +7 -2
  41. package/engines/postgresql/restore.ts +2 -2
  42. package/engines/postgresql/version-validator.ts +2 -2
  43. package/engines/sqlite/index.ts +21 -8
  44. package/package.json +1 -1
@@ -13,7 +13,7 @@ import {
13
13
  promptConfirm,
14
14
  } from '../ui/prompts'
15
15
  import { createSpinner } from '../ui/spinner'
16
- import { header, error, connectionBox } from '../ui/theme'
16
+ import { header, uiError, connectionBox } from '../ui/theme'
17
17
  import { tmpdir } from 'os'
18
18
  import { join } from 'path'
19
19
  import { getMissingDependencies } from '../../core/dependency-manager'
@@ -21,9 +21,9 @@ import { platformService } from '../../core/platform-service'
21
21
  import { startWithRetry } from '../../core/start-with-retry'
22
22
  import { TransactionManager } from '../../core/transaction-manager'
23
23
  import { isValidDatabaseName } from '../../core/error-handler'
24
+ import { resolve } from 'path'
24
25
  import { Engine } from '../../types'
25
26
  import type { BaseEngine } from '../../engines/base-engine'
26
- import { resolve } from 'path'
27
27
 
28
28
  /**
29
29
  * Simplified SQLite container creation flow
@@ -43,8 +43,13 @@ async function createSqliteContainer(
43
43
 
44
44
  const missingDeps = await getMissingDependencies('sqlite')
45
45
  if (missingDeps.length > 0) {
46
- depsSpinner.warn(`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`)
47
- const installed = await promptInstallDependencies(missingDeps[0].binary, 'sqlite')
46
+ depsSpinner.warn(
47
+ `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
48
+ )
49
+ const installed = await promptInstallDependencies(
50
+ missingDeps[0].binary,
51
+ 'sqlite',
52
+ )
48
53
  if (!installed) {
49
54
  process.exit(1)
50
55
  }
@@ -64,7 +69,7 @@ async function createSqliteContainer(
64
69
 
65
70
  // Check if file already exists
66
71
  if (existsSync(absolutePath)) {
67
- console.error(error(`File already exists: ${absolutePath}`))
72
+ console.error(uiError(`File already exists: ${absolutePath}`))
68
73
  process.exit(1)
69
74
  }
70
75
 
@@ -75,9 +80,9 @@ async function createSqliteContainer(
75
80
  // Initialize the SQLite database file and register in registry
76
81
  await dbEngine.initDataDir(containerName, version, { path: absolutePath })
77
82
  createSpinnerInstance.succeed('SQLite database created')
78
- } catch (err) {
83
+ } catch (error) {
79
84
  createSpinnerInstance.fail('Failed to create SQLite database')
80
- throw err
85
+ throw error
81
86
  }
82
87
 
83
88
  // Handle --from restore
@@ -85,15 +90,17 @@ async function createSqliteContainer(
85
90
  const config = await containerManager.getConfig(containerName)
86
91
  if (config) {
87
92
  const format = await dbEngine.detectBackupFormat(restoreLocation)
88
- const restoreSpinner = createSpinner(`Restoring from ${format.description}...`)
93
+ const restoreSpinner = createSpinner(
94
+ `Restoring from ${format.description}...`,
95
+ )
89
96
  restoreSpinner.start()
90
97
 
91
98
  try {
92
99
  await dbEngine.restore(config, restoreLocation)
93
100
  restoreSpinner.succeed('Backup restored successfully')
94
- } catch (err) {
101
+ } catch (error) {
95
102
  restoreSpinner.fail('Failed to restore backup')
96
- throw err
103
+ throw error
97
104
  }
98
105
  }
99
106
  }
@@ -146,7 +153,11 @@ function detectLocationType(location: string): {
146
153
  if (existsSync(location)) {
147
154
  // Check if it's a SQLite file (case-insensitive)
148
155
  const lowerLocation = location.toLowerCase()
149
- if (lowerLocation.endsWith('.sqlite') || lowerLocation.endsWith('.db') || lowerLocation.endsWith('.sqlite3')) {
156
+ if (
157
+ lowerLocation.endsWith('.sqlite') ||
158
+ lowerLocation.endsWith('.db') ||
159
+ lowerLocation.endsWith('.sqlite3')
160
+ ) {
150
161
  return { type: 'file', inferredEngine: Engine.SQLite }
151
162
  }
152
163
  return { type: 'file' }
@@ -158,7 +169,10 @@ function detectLocationType(location: string): {
158
169
  export const createCommand = new Command('create')
159
170
  .description('Create a new database container')
160
171
  .argument('[name]', 'Container name')
161
- .option('-e, --engine <engine>', 'Database engine (postgresql, mysql, sqlite)')
172
+ .option(
173
+ '-e, --engine <engine>',
174
+ 'Database engine (postgresql, mysql, sqlite)',
175
+ )
162
176
  .option('-v, --version <version>', 'Database version')
163
177
  .option('-d, --database <database>', 'Database name')
164
178
  .option('-p, --port <port>', 'Port number')
@@ -207,7 +221,7 @@ export const createCommand = new Command('create')
207
221
  const locationInfo = detectLocationType(options.from)
208
222
 
209
223
  if (locationInfo.type === 'not_found') {
210
- console.error(error(`Location not found: ${options.from}`))
224
+ console.error(uiError(`Location not found: ${options.from}`))
211
225
  console.log(
212
226
  chalk.gray(
213
227
  ' Provide a valid file path or connection string (postgresql://, mysql://)',
@@ -230,7 +244,7 @@ export const createCommand = new Command('create')
230
244
 
231
245
  if (options.start === false) {
232
246
  console.error(
233
- error(
247
+ uiError(
234
248
  'Cannot use --no-start with --from (restore requires running container)',
235
249
  ),
236
250
  )
@@ -257,7 +271,7 @@ export const createCommand = new Command('create')
257
271
  // Validate database name to prevent SQL injection
258
272
  if (!isValidDatabaseName(database)) {
259
273
  console.error(
260
- error(
274
+ uiError(
261
275
  'Database name must start with a letter and contain only letters, numbers, hyphens, and underscores',
262
276
  ),
263
277
  )
@@ -282,13 +296,26 @@ export const createCommand = new Command('create')
282
296
  // For server databases, validate --connect with --no-start
283
297
  if (options.connect && options.start === false) {
284
298
  console.error(
285
- error(
299
+ uiError(
286
300
  'Cannot use --no-start with --connect (connection requires running container)',
287
301
  ),
288
302
  )
289
303
  process.exit(1)
290
304
  }
291
305
 
306
+ // Validate --max-connections if provided
307
+ if (options.maxConnections) {
308
+ const parsed = parseInt(options.maxConnections, 10)
309
+ if (!Number.isFinite(parsed) || parsed <= 0) {
310
+ console.error(
311
+ uiError(
312
+ 'Invalid --max-connections value: must be a positive integer',
313
+ ),
314
+ )
315
+ process.exit(1)
316
+ }
317
+ }
318
+
292
319
  const depsSpinner = createSpinner('Checking required tools...')
293
320
  depsSpinner.start()
294
321
 
@@ -310,7 +337,7 @@ export const createCommand = new Command('create')
310
337
  missingDeps = await getMissingDependencies(engine)
311
338
  if (missingDeps.length > 0) {
312
339
  console.error(
313
- error(
340
+ uiError(
314
341
  `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
315
342
  ),
316
343
  )
@@ -399,9 +426,9 @@ export const createCommand = new Command('create')
399
426
  })
400
427
 
401
428
  createSpinnerInstance.succeed('Container created')
402
- } catch (err) {
429
+ } catch (error) {
403
430
  createSpinnerInstance.fail('Failed to create container')
404
- throw err
431
+ throw error
405
432
  }
406
433
 
407
434
  const initSpinner = createSpinner('Initializing database cluster...')
@@ -415,10 +442,10 @@ export const createCommand = new Command('create')
415
442
  : undefined,
416
443
  })
417
444
  initSpinner.succeed('Database cluster initialized')
418
- } catch (err) {
445
+ } catch (error) {
419
446
  initSpinner.fail('Failed to initialize database cluster')
420
447
  await tx.rollback()
421
- throw err
448
+ throw error
422
449
  }
423
450
 
424
451
  // --from requires start, --start forces start, --no-start skips, otherwise ask user
@@ -484,14 +511,14 @@ export const createCommand = new Command('create')
484
511
  } else {
485
512
  startSpinner.succeed(`${dbEngine.displayName} started`)
486
513
  }
487
- } catch (err) {
514
+ } catch (error) {
488
515
  if (!startSpinner.isSpinning) {
489
516
  // Error was already handled above
490
517
  } else {
491
518
  startSpinner.fail(`Failed to start ${dbEngine.displayName}`)
492
519
  }
493
520
  await tx.rollback()
494
- throw err
521
+ throw error
495
522
  }
496
523
 
497
524
  const defaultDb = engineDefaults.superuser
@@ -504,10 +531,10 @@ export const createCommand = new Command('create')
504
531
  try {
505
532
  await dbEngine.createDatabase(config, database)
506
533
  dbSpinner.succeed(`Database "${database}" created`)
507
- } catch (err) {
534
+ } catch (error) {
508
535
  dbSpinner.fail(`Failed to create database "${database}"`)
509
536
  await tx.rollback()
510
- throw err
537
+ throw error
511
538
  }
512
539
  }
513
540
  }
@@ -538,8 +565,8 @@ export const createCommand = new Command('create')
538
565
  dumpSpinner.succeed('Dump created from remote database')
539
566
  backupPath = tempDumpPath
540
567
  dumpSuccess = true
541
- } catch (err) {
542
- const e = err as Error
568
+ } catch (error) {
569
+ const e = error as Error
543
570
  dumpSpinner.fail('Failed to create dump')
544
571
 
545
572
  if (
@@ -554,14 +581,14 @@ export const createCommand = new Command('create')
554
581
  }
555
582
 
556
583
  console.log()
557
- console.error(error('pg_dump error:'))
584
+ console.error(uiError('pg_dump error:'))
558
585
  console.log(chalk.gray(` ${e.message}`))
559
586
  process.exit(1)
560
587
  }
561
588
  }
562
589
 
563
590
  if (!dumpSuccess) {
564
- console.error(error('Failed to create dump after retries'))
591
+ console.error(uiError('Failed to create dump after retries'))
565
592
  process.exit(1)
566
593
  }
567
594
  } else {
@@ -639,8 +666,8 @@ export const createCommand = new Command('create')
639
666
  console.log()
640
667
  }
641
668
  }
642
- } catch (err) {
643
- const e = err as Error
669
+ } catch (error) {
670
+ const e = error as Error
644
671
 
645
672
  const missingToolPatterns = [
646
673
  'pg_restore not found',
@@ -666,7 +693,7 @@ export const createCommand = new Command('create')
666
693
  process.exit(1)
667
694
  }
668
695
 
669
- console.error(error(e.message))
696
+ console.error(uiError(e.message))
670
697
  process.exit(1)
671
698
  } finally {
672
699
  if (tempDumpPath) {
@@ -4,7 +4,7 @@ import { processManager } from '../../core/process-manager'
4
4
  import { getEngine } from '../../engines'
5
5
  import { promptContainerSelect, promptConfirm } from '../ui/prompts'
6
6
  import { createSpinner } from '../ui/spinner'
7
- import { error, warning } from '../ui/theme'
7
+ import { uiError, uiWarning } from '../ui/theme'
8
8
 
9
9
  export const deleteCommand = new Command('delete')
10
10
  .alias('rm')
@@ -24,7 +24,7 @@ export const deleteCommand = new Command('delete')
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,7 +38,7 @@ export const deleteCommand = new Command('delete')
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
 
@@ -48,7 +48,7 @@ export const deleteCommand = new Command('delete')
48
48
  false,
49
49
  )
50
50
  if (!confirmed) {
51
- console.log(warning('Deletion cancelled'))
51
+ console.log(uiWarning('Deletion cancelled'))
52
52
  return
53
53
  }
54
54
  }
@@ -67,7 +67,7 @@ export const deleteCommand = new Command('delete')
67
67
  stopSpinner.succeed(`Stopped "${containerName}"`)
68
68
  } else {
69
69
  console.error(
70
- error(
70
+ uiError(
71
71
  `Container "${containerName}" is running. Stop it first or use --force`,
72
72
  ),
73
73
  )
@@ -81,9 +81,9 @@ export const deleteCommand = new Command('delete')
81
81
  await containerManager.delete(containerName, { force: true })
82
82
 
83
83
  deleteSpinner.succeed(`Container "${containerName}" deleted`)
84
- } catch (err) {
85
- const e = err as Error
86
- console.error(error(e.message))
84
+ } catch (error) {
85
+ const e = error as Error
86
+ console.error(uiError(e.message))
87
87
  process.exit(1)
88
88
  }
89
89
  },
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
- import { header, success, warning, error } from '../ui/theme'
3
+ import { header, uiSuccess, uiWarning, uiError } from '../ui/theme'
4
4
  import { createSpinner } from '../ui/spinner'
5
5
  import {
6
6
  detectPackageManager,
@@ -84,7 +84,7 @@ depsCommand
84
84
  // Check specific engine
85
85
  const engineConfig = getEngineDependencies(options.engine)
86
86
  if (!engineConfig) {
87
- console.error(error(`Unknown engine: ${options.engine}`))
87
+ console.error(uiError(`Unknown engine: ${options.engine}`))
88
88
  console.log(
89
89
  chalk.gray(
90
90
  ` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
@@ -104,9 +104,9 @@ depsCommand
104
104
  const total = statuses.length
105
105
  console.log()
106
106
  if (installed === total) {
107
- console.log(success(`All ${total} dependencies installed`))
107
+ console.log(uiSuccess(`All ${total} dependencies installed`))
108
108
  } else {
109
- console.log(warning(`${installed}/${total} dependencies installed`))
109
+ console.log(uiWarning(`${installed}/${total} dependencies installed`))
110
110
  console.log()
111
111
  console.log(
112
112
  chalk.gray(` Run: spindb deps install --engine ${options.engine}`),
@@ -132,7 +132,7 @@ depsCommand
132
132
  const packageManager = await detectPackageManager()
133
133
 
134
134
  if (!packageManager) {
135
- console.log(error('No supported package manager detected'))
135
+ console.log(uiError('No supported package manager detected'))
136
136
  console.log()
137
137
 
138
138
  const platform = getCurrentPlatform()
@@ -161,7 +161,7 @@ depsCommand
161
161
  const missing = await getAllMissingDependencies()
162
162
 
163
163
  if (missing.length === 0) {
164
- console.log(success('All dependencies are already installed'))
164
+ console.log(uiSuccess('All dependencies are already installed'))
165
165
  return
166
166
  }
167
167
 
@@ -182,14 +182,14 @@ depsCommand
182
182
  spinner.warn('Some dependencies failed to install')
183
183
  console.log()
184
184
  for (const f of failed) {
185
- console.log(error(` ${f.dependency.name}: ${f.error}`))
185
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
186
186
  }
187
187
  }
188
188
 
189
189
  if (succeeded.length > 0) {
190
190
  console.log()
191
191
  console.log(
192
- success(
192
+ uiSuccess(
193
193
  `Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
194
194
  ),
195
195
  )
@@ -198,7 +198,7 @@ depsCommand
198
198
  // Install dependencies for specific engine
199
199
  const engineConfig = getEngineDependencies(options.engine)
200
200
  if (!engineConfig) {
201
- console.error(error(`Unknown engine: ${options.engine}`))
201
+ console.error(uiError(`Unknown engine: ${options.engine}`))
202
202
  console.log(
203
203
  chalk.gray(
204
204
  ` Available engines: ${engineDependencies.map((e) => e.engine).join(', ')}`,
@@ -211,7 +211,9 @@ depsCommand
211
211
 
212
212
  if (missing.length === 0) {
213
213
  console.log(
214
- success(`All ${engineConfig.displayName} dependencies are installed`),
214
+ uiSuccess(
215
+ `All ${engineConfig.displayName} dependencies are installed`,
216
+ ),
215
217
  )
216
218
  return
217
219
  }
@@ -241,7 +243,7 @@ depsCommand
241
243
  spinner.warn('Some dependencies failed to install')
242
244
  console.log()
243
245
  for (const f of failed) {
244
- console.log(error(` ${f.dependency.name}: ${f.error}`))
246
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
245
247
  }
246
248
 
247
249
  // Show manual instructions
@@ -259,7 +261,7 @@ depsCommand
259
261
  if (succeeded.length > 0) {
260
262
  console.log()
261
263
  console.log(
262
- success(
264
+ uiSuccess(
263
265
  `Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
264
266
  ),
265
267
  )
@@ -276,7 +278,7 @@ depsCommand
276
278
  const missing = await getMissingDependencies('postgresql')
277
279
 
278
280
  if (missing.length === 0) {
279
- console.log(success('All PostgreSQL dependencies are installed'))
281
+ console.log(uiSuccess('All PostgreSQL dependencies are installed'))
280
282
  return
281
283
  }
282
284
 
@@ -300,14 +302,14 @@ depsCommand
300
302
  spinner.warn('Some dependencies failed to install')
301
303
  console.log()
302
304
  for (const f of failed) {
303
- console.log(error(` ${f.dependency.name}: ${f.error}`))
305
+ console.log(uiError(` ${f.dependency.name}: ${f.error}`))
304
306
  }
305
307
  }
306
308
 
307
309
  if (succeeded.length > 0) {
308
310
  console.log()
309
311
  console.log(
310
- success(
312
+ uiSuccess(
311
313
  `Installed: ${succeeded.map((r) => r.dependency.name).join(', ')}`,
312
314
  ),
313
315
  )
@@ -18,7 +18,7 @@ import { sqliteRegistry } from '../../engines/sqlite/registry'
18
18
  import { paths } from '../../config/paths'
19
19
  import { getSupportedEngines } from '../../config/engine-defaults'
20
20
  import { checkEngineDependencies } from '../../core/dependency-manager'
21
- import { header, success } from '../ui/theme'
21
+ import { header, uiSuccess } from '../ui/theme'
22
22
  import { Engine } from '../../types'
23
23
 
24
24
  type HealthCheckResult = {
@@ -61,7 +61,7 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
61
61
  label: 'Refresh binary cache',
62
62
  handler: async () => {
63
63
  await configManager.refreshAllBinaries()
64
- console.log(success('Binary cache refreshed'))
64
+ console.log(uiSuccess('Binary cache refreshed'))
65
65
  },
66
66
  },
67
67
  }
@@ -73,12 +73,12 @@ async function checkConfiguration(): Promise<HealthCheckResult> {
73
73
  message: 'Configuration valid',
74
74
  details: [`Binary tools cached: ${binaryCount}`],
75
75
  }
76
- } catch (err) {
76
+ } catch (error) {
77
77
  return {
78
78
  name: 'Configuration',
79
79
  status: 'error',
80
80
  message: 'Configuration file is corrupted',
81
- details: [(err as Error).message],
81
+ details: [(error as Error).message],
82
82
  }
83
83
  }
84
84
  }
@@ -125,12 +125,12 @@ async function checkContainers(): Promise<HealthCheckResult> {
125
125
  message: `${containers.length} container(s)`,
126
126
  details,
127
127
  }
128
- } catch (err) {
128
+ } catch (error) {
129
129
  return {
130
130
  name: 'Containers',
131
131
  status: 'error',
132
132
  message: 'Failed to list containers',
133
- details: [(err as Error).message],
133
+ details: [(error as Error).message],
134
134
  }
135
135
  }
136
136
  }
@@ -162,7 +162,7 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
162
162
  label: 'Remove orphaned entries from registry',
163
163
  handler: async () => {
164
164
  const count = await sqliteRegistry.removeOrphans()
165
- console.log(success(`Removed ${count} orphaned entries`))
165
+ console.log(uiSuccess(`Removed ${count} orphaned entries`))
166
166
  },
167
167
  },
168
168
  }
@@ -173,12 +173,12 @@ async function checkSqliteRegistry(): Promise<HealthCheckResult> {
173
173
  status: 'ok',
174
174
  message: `${entries.length} database(s) registered, all files exist`,
175
175
  }
176
- } catch (err) {
176
+ } catch (error) {
177
177
  return {
178
178
  name: 'SQLite Registry',
179
179
  status: 'warning',
180
180
  message: 'Could not check registry',
181
- details: [(err as Error).message],
181
+ details: [(error as Error).message],
182
182
  }
183
183
  }
184
184
  }
@@ -211,12 +211,12 @@ async function checkBinaries(): Promise<HealthCheckResult> {
211
211
  message: hasWarning ? 'Some tools missing' : 'All tools available',
212
212
  details: results,
213
213
  }
214
- } catch (err) {
214
+ } catch (error) {
215
215
  return {
216
216
  name: 'Database Tools',
217
217
  status: 'error',
218
218
  message: 'Failed to check tools',
219
- details: [(err as Error).message],
219
+ details: [(error as Error).message],
220
220
  }
221
221
  }
222
222
  }