spindb 0.5.3 → 0.5.4

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.
@@ -33,7 +33,19 @@ import { defaults } from '../../config/defaults'
33
33
  import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
34
34
  import type { EngineName } from '../../types'
35
35
  import inquirer from 'inquirer'
36
- import { getMissingDependencies } from '../../core/dependency-manager'
36
+ import {
37
+ getMissingDependencies,
38
+ isUsqlInstalled,
39
+ isPgcliInstalled,
40
+ isMycliInstalled,
41
+ detectPackageManager,
42
+ installUsql,
43
+ installPgcli,
44
+ installMycli,
45
+ getUsqlManualInstructions,
46
+ getPgcliManualInstructions,
47
+ getMycliManualInstructions,
48
+ } from '../../core/dependency-manager'
37
49
  import {
38
50
  getMysqldPath,
39
51
  getMysqlVersion,
@@ -57,6 +69,19 @@ const engineIcons: Record<string, string> = {
57
69
  mysql: '🐬',
58
70
  }
59
71
 
72
+ /**
73
+ * Helper to pause and wait for user to press Enter
74
+ */
75
+ async function pressEnterToContinue(): Promise<void> {
76
+ await inquirer.prompt([
77
+ {
78
+ type: 'input',
79
+ name: 'continue',
80
+ message: chalk.gray('Press Enter to continue...'),
81
+ },
82
+ ])
83
+ }
84
+
60
85
  async function showMainMenu(): Promise<void> {
61
86
  console.clear()
62
87
  console.log(header('SpinDB - Local Database Manager'))
@@ -88,12 +113,12 @@ async function showMainMenu(): Promise<void> {
88
113
  const choices: MenuChoice[] = [
89
114
  ...(hasContainers
90
115
  ? [
91
- { name: `${chalk.cyan('◉')} List containers`, value: 'list' },
116
+ { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
92
117
  { name: `${chalk.green('+')} Create new container`, value: 'create' },
93
118
  ]
94
119
  : [
95
120
  { name: `${chalk.green('+')} Create new container`, value: 'create' },
96
- { name: `${chalk.cyan('◉')} List containers`, value: 'list' },
121
+ { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
97
122
  ]),
98
123
  {
99
124
  name: canStart
@@ -415,7 +440,7 @@ async function handleList(): Promise<void> {
415
440
  console.log()
416
441
  const containerChoices = [
417
442
  ...containers.map((c) => ({
418
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${
443
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || ''} ${c.engine} ${c.version}, port ${c.port})`)} ${
419
444
  c.status === 'running'
420
445
  ? chalk.green('● running')
421
446
  : chalk.gray('○ stopped')
@@ -435,6 +460,7 @@ async function handleList(): Promise<void> {
435
460
  name: 'selectedContainer',
436
461
  message: 'Select a container for more options:',
437
462
  choices: containerChoices,
463
+ pageSize: 15,
438
464
  },
439
465
  ])
440
466
 
@@ -473,7 +499,7 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
473
499
  // Start or Stop depending on current state
474
500
  !isRunning
475
501
  ? { name: `${chalk.green('▶')} Start container`, value: 'start' }
476
- : { name: `${chalk.yellow('■')} Stop container`, value: 'stop' },
502
+ : { name: `${chalk.red('■')} Stop container`, value: 'stop' },
477
503
  {
478
504
  name: isRunning
479
505
  ? `${chalk.blue('⌘')} Open shell`
@@ -496,10 +522,16 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
496
522
  disabled: !isRunning ? false : 'Stop container first',
497
523
  },
498
524
  { name: `${chalk.magenta('⎘')} Copy connection string`, value: 'copy' },
499
- { name: `${chalk.red('✕')} Delete container`, value: 'delete' },
525
+ {
526
+ name: !isRunning
527
+ ? `${chalk.red('✕')} Delete container`
528
+ : chalk.gray('✕ Delete container'),
529
+ value: 'delete',
530
+ disabled: !isRunning ? false : 'Stop container first',
531
+ },
500
532
  new inquirer.Separator(),
501
- { name: `${chalk.blue('←')} Back to container list`, value: 'back' },
502
- { name: `${chalk.blue('🏠')} Back to main menu`, value: 'main' },
533
+ { name: `${chalk.blue('←')} Back to containers`, value: 'back' },
534
+ { name: `${chalk.blue('')} Back to main menu`, value: 'main' },
503
535
  ]
504
536
 
505
537
  const { action } = await inquirer.prompt<{ action: string }>([
@@ -508,6 +540,7 @@ async function showContainerSubmenu(containerName: string): Promise<void> {
508
540
  name: 'action',
509
541
  message: 'What would you like to do?',
510
542
  choices: actionChoices,
543
+ pageSize: 15,
511
544
  },
512
545
  ])
513
546
 
@@ -680,15 +713,222 @@ async function handleOpenShell(containerName: string): Promise<void> {
680
713
  const engine = getEngine(config.engine)
681
714
  const connectionString = engine.getConnectionString(config)
682
715
 
716
+ // Check which enhanced shells are installed
717
+ const usqlInstalled = await isUsqlInstalled()
718
+ const pgcliInstalled = await isPgcliInstalled()
719
+ const mycliInstalled = await isMycliInstalled()
720
+
721
+ type ShellChoice =
722
+ | 'default'
723
+ | 'usql'
724
+ | 'install-usql'
725
+ | 'pgcli'
726
+ | 'install-pgcli'
727
+ | 'mycli'
728
+ | 'install-mycli'
729
+ | 'back'
730
+
731
+ const defaultShellName = config.engine === 'mysql' ? 'mysql' : 'psql'
732
+ const engineSpecificCli = config.engine === 'mysql' ? 'mycli' : 'pgcli'
733
+ const engineSpecificInstalled =
734
+ config.engine === 'mysql' ? mycliInstalled : pgcliInstalled
735
+
736
+ const choices: Array<{ name: string; value: ShellChoice }> = [
737
+ {
738
+ name: `>_ Use default shell (${defaultShellName})`,
739
+ value: 'default',
740
+ },
741
+ ]
742
+
743
+ // Engine-specific enhanced CLI (pgcli for PostgreSQL, mycli for MySQL)
744
+ if (engineSpecificInstalled) {
745
+ choices.push({
746
+ name: `⚡ Use ${engineSpecificCli} (enhanced features, recommended)`,
747
+ value: config.engine === 'mysql' ? 'mycli' : 'pgcli',
748
+ })
749
+ } else {
750
+ choices.push({
751
+ name: `↓ Install ${engineSpecificCli} (enhanced features, recommended)`,
752
+ value: config.engine === 'mysql' ? 'install-mycli' : 'install-pgcli',
753
+ })
754
+ }
755
+
756
+ // usql - universal option
757
+ if (usqlInstalled) {
758
+ choices.push({
759
+ name: '⚡ Use usql (universal SQL client)',
760
+ value: 'usql',
761
+ })
762
+ } else {
763
+ choices.push({
764
+ name: '↓ Install usql (universal SQL client)',
765
+ value: 'install-usql',
766
+ })
767
+ }
768
+
769
+ choices.push({
770
+ name: `${chalk.blue('←')} Back`,
771
+ value: 'back',
772
+ })
773
+
774
+ const { shellChoice } = await inquirer.prompt<{ shellChoice: ShellChoice }>([
775
+ {
776
+ type: 'list',
777
+ name: 'shellChoice',
778
+ message: 'Select shell option:',
779
+ choices,
780
+ pageSize: 10,
781
+ },
782
+ ])
783
+
784
+ if (shellChoice === 'back') {
785
+ return
786
+ }
787
+
788
+ // Handle pgcli installation
789
+ if (shellChoice === 'install-pgcli') {
790
+ console.log()
791
+ console.log(info('Installing pgcli for enhanced PostgreSQL shell...'))
792
+ const pm = await detectPackageManager()
793
+ if (pm) {
794
+ const result = await installPgcli(pm)
795
+ if (result.success) {
796
+ console.log(success('pgcli installed successfully!'))
797
+ console.log()
798
+ await launchShell(containerName, config, connectionString, 'pgcli')
799
+ } else {
800
+ console.error(error(`Failed to install pgcli: ${result.error}`))
801
+ console.log()
802
+ console.log(chalk.gray('Manual installation:'))
803
+ for (const instruction of getPgcliManualInstructions()) {
804
+ console.log(chalk.cyan(` ${instruction}`))
805
+ }
806
+ console.log()
807
+ await pressEnterToContinue()
808
+ }
809
+ } else {
810
+ console.error(error('No supported package manager found'))
811
+ console.log()
812
+ console.log(chalk.gray('Manual installation:'))
813
+ for (const instruction of getPgcliManualInstructions()) {
814
+ console.log(chalk.cyan(` ${instruction}`))
815
+ }
816
+ console.log()
817
+ await pressEnterToContinue()
818
+ }
819
+ return
820
+ }
821
+
822
+ // Handle mycli installation
823
+ if (shellChoice === 'install-mycli') {
824
+ console.log()
825
+ console.log(info('Installing mycli for enhanced MySQL shell...'))
826
+ const pm = await detectPackageManager()
827
+ if (pm) {
828
+ const result = await installMycli(pm)
829
+ if (result.success) {
830
+ console.log(success('mycli installed successfully!'))
831
+ console.log()
832
+ await launchShell(containerName, config, connectionString, 'mycli')
833
+ } else {
834
+ console.error(error(`Failed to install mycli: ${result.error}`))
835
+ console.log()
836
+ console.log(chalk.gray('Manual installation:'))
837
+ for (const instruction of getMycliManualInstructions()) {
838
+ console.log(chalk.cyan(` ${instruction}`))
839
+ }
840
+ console.log()
841
+ await pressEnterToContinue()
842
+ }
843
+ } else {
844
+ console.error(error('No supported package manager found'))
845
+ console.log()
846
+ console.log(chalk.gray('Manual installation:'))
847
+ for (const instruction of getMycliManualInstructions()) {
848
+ console.log(chalk.cyan(` ${instruction}`))
849
+ }
850
+ console.log()
851
+ await pressEnterToContinue()
852
+ }
853
+ return
854
+ }
855
+
856
+ // Handle usql installation
857
+ if (shellChoice === 'install-usql') {
858
+ console.log()
859
+ console.log(info('Installing usql for enhanced shell experience...'))
860
+ const pm = await detectPackageManager()
861
+ if (pm) {
862
+ const result = await installUsql(pm)
863
+ if (result.success) {
864
+ console.log(success('usql installed successfully!'))
865
+ console.log()
866
+ await launchShell(containerName, config, connectionString, 'usql')
867
+ } else {
868
+ console.error(error(`Failed to install usql: ${result.error}`))
869
+ console.log()
870
+ console.log(chalk.gray('Manual installation:'))
871
+ for (const instruction of getUsqlManualInstructions()) {
872
+ console.log(chalk.cyan(` ${instruction}`))
873
+ }
874
+ console.log()
875
+ await pressEnterToContinue()
876
+ }
877
+ } else {
878
+ console.error(error('No supported package manager found'))
879
+ console.log()
880
+ console.log(chalk.gray('Manual installation:'))
881
+ for (const instruction of getUsqlManualInstructions()) {
882
+ console.log(chalk.cyan(` ${instruction}`))
883
+ }
884
+ console.log()
885
+ await pressEnterToContinue()
886
+ }
887
+ return
888
+ }
889
+
890
+ // Launch the selected shell
891
+ await launchShell(containerName, config, connectionString, shellChoice)
892
+ }
893
+
894
+ async function launchShell(
895
+ containerName: string,
896
+ config: NonNullable<Awaited<ReturnType<typeof containerManager.getConfig>>>,
897
+ connectionString: string,
898
+ shellType: 'default' | 'usql' | 'pgcli' | 'mycli',
899
+ ): Promise<void> {
683
900
  console.log(info(`Connecting to ${containerName}...`))
684
901
  console.log()
685
902
 
686
- // Determine shell command based on engine
903
+ // Determine shell command based on engine and shell type
687
904
  let shellCmd: string
688
905
  let shellArgs: string[]
689
906
  let installHint: string
690
907
 
691
- if (config.engine === 'mysql') {
908
+ if (shellType === 'pgcli') {
909
+ // pgcli accepts connection strings
910
+ shellCmd = 'pgcli'
911
+ shellArgs = [connectionString]
912
+ installHint = 'brew install pgcli'
913
+ } else if (shellType === 'mycli') {
914
+ // mycli: mycli -h host -P port -u user database
915
+ shellCmd = 'mycli'
916
+ shellArgs = [
917
+ '-h',
918
+ '127.0.0.1',
919
+ '-P',
920
+ String(config.port),
921
+ '-u',
922
+ 'root',
923
+ config.database,
924
+ ]
925
+ installHint = 'brew install mycli'
926
+ } else if (shellType === 'usql') {
927
+ // usql accepts connection strings directly for both PostgreSQL and MySQL
928
+ shellCmd = 'usql'
929
+ shellArgs = [connectionString]
930
+ installHint = 'brew tap xo/xo && brew install xo/xo/usql'
931
+ } else if (config.engine === 'mysql') {
692
932
  shellCmd = 'mysql'
693
933
  // MySQL connection: mysql -u root -h 127.0.0.1 -P port database
694
934
  shellArgs = [
@@ -719,7 +959,7 @@ async function handleOpenShell(containerName: string): Promise<void> {
719
959
  console.log(chalk.gray(' Connect manually with:'))
720
960
  console.log(chalk.cyan(` ${connectionString}`))
721
961
  console.log()
722
- console.log(chalk.gray(` Install ${config.engine} client:`))
962
+ console.log(chalk.gray(` Install ${shellCmd}:`))
723
963
  console.log(chalk.cyan(` ${installHint}`))
724
964
  }
725
965
  })
@@ -842,7 +1082,7 @@ async function handleRestore(): Promise<void> {
842
1082
  // Build choices: running containers + create new option
843
1083
  const choices = [
844
1084
  ...running.map((c) => ({
845
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
1085
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || ''} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
846
1086
  value: c.name,
847
1087
  short: c.name,
848
1088
  })),
@@ -862,6 +1102,7 @@ async function handleRestore(): Promise<void> {
862
1102
  name: 'selectedContainer',
863
1103
  message: 'Select container to restore to:',
864
1104
  choices,
1105
+ pageSize: 15,
865
1106
  },
866
1107
  ])
867
1108
 
@@ -1056,7 +1297,7 @@ async function handleRestore(): Promise<void> {
1056
1297
  backupPath = stripQuotes(rawBackupPath)
1057
1298
  }
1058
1299
 
1059
- const databaseName = await promptDatabaseName(containerName)
1300
+ const databaseName = await promptDatabaseName(containerName, config.engine)
1060
1301
 
1061
1302
  const engine = getEngine(config.engine)
1062
1303
 
@@ -1437,7 +1678,7 @@ async function handleEditContainer(
1437
1678
  },
1438
1679
  new inquirer.Separator(),
1439
1680
  { name: `${chalk.blue('←')} Back to container`, value: 'back' },
1440
- { name: `${chalk.blue('🏠')} Back to main menu`, value: 'main' },
1681
+ { name: `${chalk.blue('')} Back to main menu`, value: 'main' },
1441
1682
  ]
1442
1683
 
1443
1684
  const { field } = await inquirer.prompt<{ field: string }>([
@@ -1446,6 +1687,7 @@ async function handleEditContainer(
1446
1687
  name: 'field',
1447
1688
  message: 'Select field to edit:',
1448
1689
  choices: editChoices,
1690
+ pageSize: 10,
1449
1691
  },
1450
1692
  ])
1451
1693
 
@@ -1809,7 +2051,7 @@ async function handleEngines(): Promise<void> {
1809
2051
 
1810
2052
  // PostgreSQL rows
1811
2053
  for (const engine of pgEngines) {
1812
- const icon = engineIcons[engine.engine] || '🗄️'
2054
+ const icon = engineIcons[engine.engine] || ''
1813
2055
  const platformInfo = `${engine.platform}-${engine.arch}`
1814
2056
 
1815
2057
  console.log(
@@ -246,7 +246,7 @@ export const restoreCommand = new Command('restore')
246
246
  // Get database name
247
247
  let databaseName = options.database
248
248
  if (!databaseName) {
249
- databaseName = await promptDatabaseName(containerName)
249
+ databaseName = await promptDatabaseName(containerName, engineName)
250
250
  }
251
251
 
252
252
  // At this point backupPath is guaranteed to be set
package/cli/ui/prompts.ts CHANGED
@@ -53,7 +53,7 @@ export async function promptEngine(): Promise<string> {
53
53
 
54
54
  // Build choices from available engines
55
55
  const choices = engines.map((e) => ({
56
- name: `${engineIcons[e.name] || '🗄️'} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
56
+ name: `${engineIcons[e.name] || ''} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
57
57
  value: e.name,
58
58
  short: e.displayName,
59
59
  }))
@@ -227,7 +227,7 @@ export async function promptContainerSelect(
227
227
  name: 'container',
228
228
  message,
229
229
  choices: containers.map((c) => ({
230
- name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine} ${c.version}, port ${c.port})`)} ${
230
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || ''} ${c.engine} ${c.version}, port ${c.port})`)} ${
231
231
  c.status === 'running'
232
232
  ? chalk.green('● running')
233
233
  : chalk.gray('○ stopped')
@@ -243,19 +243,25 @@ export async function promptContainerSelect(
243
243
 
244
244
  /**
245
245
  * Prompt for database name
246
+ * @param defaultName - Default value for the database name
247
+ * @param engine - Database engine (mysql shows "schema" terminology)
246
248
  */
247
249
  export async function promptDatabaseName(
248
250
  defaultName?: string,
251
+ engine?: string,
249
252
  ): Promise<string> {
253
+ // MySQL uses "schema" terminology (database and schema are synonymous)
254
+ const label = engine === 'mysql' ? 'Database (schema) name:' : 'Database name:'
255
+
250
256
  const { database } = await inquirer.prompt<{ database: string }>([
251
257
  {
252
258
  type: 'input',
253
259
  name: 'database',
254
- message: 'Database name:',
260
+ message: label,
255
261
  default: defaultName,
256
262
  validate: (input: string) => {
257
263
  if (!input) return 'Database name is required'
258
- // PostgreSQL database naming rules
264
+ // PostgreSQL database naming rules (also valid for MySQL)
259
265
  if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(input)) {
260
266
  return 'Database name must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens'
261
267
  }
@@ -282,12 +288,12 @@ export type CreateOptions = {
282
288
  * Full interactive create flow
283
289
  */
284
290
  export async function promptCreateOptions(): Promise<CreateOptions> {
285
- console.log(chalk.cyan('\n 🗄️ Create New Database Container\n'))
291
+ console.log(chalk.cyan('\n Create New Database Container\n'))
286
292
 
287
293
  const engine = await promptEngine()
288
294
  const version = await promptVersion(engine)
289
295
  const name = await promptContainerName()
290
- const database = await promptDatabaseName(name) // Default to container name
296
+ const database = await promptDatabaseName(name, engine) // Default to container name
291
297
 
292
298
  // Get engine-specific default port
293
299
  const engineDefaults = getEngineDefaults(engine)
package/cli/ui/theme.ts CHANGED
@@ -40,7 +40,7 @@ export const theme = {
40
40
  info: chalk.blue('ℹ'),
41
41
  arrow: chalk.cyan('→'),
42
42
  bullet: chalk.gray('•'),
43
- database: '🗄️',
43
+ database: '',
44
44
  postgres: '🐘',
45
45
  },
46
46
  }
@@ -289,6 +289,98 @@ const mysqlDependencies: EngineDependencies = {
289
289
  ],
290
290
  }
291
291
 
292
+ // =============================================================================
293
+ // Optional Tools (engine-agnostic)
294
+ // =============================================================================
295
+
296
+ /**
297
+ * usql - Universal SQL client
298
+ * Works with PostgreSQL, MySQL, SQLite, and 20+ other databases
299
+ * https://github.com/xo/usql
300
+ */
301
+ export const usqlDependency: Dependency = {
302
+ name: 'usql',
303
+ binary: 'usql',
304
+ description:
305
+ 'Universal SQL client with auto-completion, syntax highlighting, and multi-database support',
306
+ packages: {
307
+ brew: {
308
+ package: 'xo/xo/usql',
309
+ preInstall: ['brew tap xo/xo'],
310
+ },
311
+ // Note: usql is not in standard Linux package repos, must use manual install
312
+ },
313
+ manualInstall: {
314
+ darwin: [
315
+ 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
316
+ 'Then run: brew tap xo/xo && brew install xo/xo/usql',
317
+ ],
318
+ linux: [
319
+ 'Download from GitHub releases: https://github.com/xo/usql/releases',
320
+ 'Extract and move to PATH: sudo mv usql /usr/local/bin/',
321
+ 'Or install via Go: go install github.com/xo/usql@latest',
322
+ ],
323
+ },
324
+ }
325
+
326
+ /**
327
+ * pgcli - PostgreSQL CLI with auto-completion and syntax highlighting
328
+ * https://github.com/dbcli/pgcli
329
+ */
330
+ export const pgcliDependency: Dependency = {
331
+ name: 'pgcli',
332
+ binary: 'pgcli',
333
+ description:
334
+ 'PostgreSQL CLI with intelligent auto-completion and syntax highlighting',
335
+ packages: {
336
+ brew: { package: 'pgcli' },
337
+ apt: { package: 'pgcli' },
338
+ dnf: { package: 'pgcli' },
339
+ yum: { package: 'pgcli' },
340
+ pacman: { package: 'pgcli' },
341
+ },
342
+ manualInstall: {
343
+ darwin: [
344
+ 'Install with Homebrew: brew install pgcli',
345
+ 'Or with pip: pip install pgcli',
346
+ ],
347
+ linux: [
348
+ 'Debian/Ubuntu: sudo apt install pgcli',
349
+ 'Fedora: sudo dnf install pgcli',
350
+ 'Or with pip: pip install pgcli',
351
+ ],
352
+ },
353
+ }
354
+
355
+ /**
356
+ * mycli - MySQL CLI with auto-completion and syntax highlighting
357
+ * https://github.com/dbcli/mycli
358
+ */
359
+ export const mycliDependency: Dependency = {
360
+ name: 'mycli',
361
+ binary: 'mycli',
362
+ description:
363
+ 'MySQL/MariaDB CLI with intelligent auto-completion and syntax highlighting',
364
+ packages: {
365
+ brew: { package: 'mycli' },
366
+ apt: { package: 'mycli' },
367
+ dnf: { package: 'mycli' },
368
+ yum: { package: 'mycli' },
369
+ pacman: { package: 'mycli' },
370
+ },
371
+ manualInstall: {
372
+ darwin: [
373
+ 'Install with Homebrew: brew install mycli',
374
+ 'Or with pip: pip install mycli',
375
+ ],
376
+ linux: [
377
+ 'Debian/Ubuntu: sudo apt install mycli',
378
+ 'Fedora: sudo dnf install mycli',
379
+ 'Or with pip: pip install mycli',
380
+ ],
381
+ },
382
+ }
383
+
292
384
  // =============================================================================
293
385
  // Registry
294
386
  // =============================================================================
@@ -54,26 +54,19 @@ export class BinaryManager {
54
54
  return version
55
55
  }
56
56
 
57
- /**
58
- * Get major version from any version string (e.g., "17.7.0" -> "17", "16" -> "16")
59
- * Used for directory naming to ensure one directory per major version.
60
- */
61
- getMajorVersion(version: string): string {
62
- return version.split('.')[0]
63
- }
64
-
65
57
  /**
66
58
  * Check if binaries for a specific version are already installed
59
+ * Uses full version for directory naming (e.g., postgresql-17.7.0-darwin-arm64)
67
60
  */
68
61
  async isInstalled(
69
62
  version: string,
70
63
  platform: string,
71
64
  arch: string,
72
65
  ): Promise<boolean> {
73
- const majorVersion = this.getMajorVersion(version)
66
+ const fullVersion = this.getFullVersion(version)
74
67
  const binPath = paths.getBinaryPath({
75
68
  engine: 'postgresql',
76
- version: majorVersion,
69
+ version: fullVersion,
77
70
  platform,
78
71
  arch,
79
72
  })
@@ -122,15 +115,15 @@ export class BinaryManager {
122
115
  arch: string,
123
116
  onProgress?: ProgressCallback,
124
117
  ): Promise<string> {
125
- const majorVersion = this.getMajorVersion(version)
118
+ const fullVersion = this.getFullVersion(version)
126
119
  const url = this.getDownloadUrl(version, platform, arch)
127
120
  const binPath = paths.getBinaryPath({
128
121
  engine: 'postgresql',
129
- version: majorVersion,
122
+ version: fullVersion,
130
123
  platform,
131
124
  arch,
132
125
  })
133
- const tempDir = join(paths.bin, `temp-${majorVersion}-${platform}-${arch}`)
126
+ const tempDir = join(paths.bin, `temp-${fullVersion}-${platform}-${arch}`)
134
127
  const jarFile = join(tempDir, 'postgres.jar')
135
128
 
136
129
  // Ensure directories exist
@@ -210,10 +203,10 @@ export class BinaryManager {
210
203
  platform: string,
211
204
  arch: string,
212
205
  ): Promise<boolean> {
213
- const majorVersion = this.getMajorVersion(version)
206
+ const fullVersion = this.getFullVersion(version)
214
207
  const binPath = paths.getBinaryPath({
215
208
  engine: 'postgresql',
216
- version: majorVersion,
209
+ version: fullVersion,
217
210
  platform,
218
211
  arch,
219
212
  })
@@ -267,10 +260,10 @@ export class BinaryManager {
267
260
  arch: string,
268
261
  binary: string,
269
262
  ): string {
270
- const majorVersion = this.getMajorVersion(version)
263
+ const fullVersion = this.getFullVersion(version)
271
264
  const binPath = paths.getBinaryPath({
272
265
  engine: 'postgresql',
273
- version: majorVersion,
266
+ version: fullVersion,
274
267
  platform,
275
268
  arch,
276
269
  })
@@ -286,7 +279,7 @@ export class BinaryManager {
286
279
  arch: string,
287
280
  onProgress?: ProgressCallback,
288
281
  ): Promise<string> {
289
- const majorVersion = this.getMajorVersion(version)
282
+ const fullVersion = this.getFullVersion(version)
290
283
  if (await this.isInstalled(version, platform, arch)) {
291
284
  onProgress?.({
292
285
  stage: 'cached',
@@ -294,7 +287,7 @@ export class BinaryManager {
294
287
  })
295
288
  return paths.getBinaryPath({
296
289
  engine: 'postgresql',
297
- version: majorVersion,
290
+ version: fullVersion,
298
291
  platform,
299
292
  arch,
300
293
  })