spindb 0.5.2 → 0.5.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 (36) hide show
  1. package/README.md +137 -8
  2. package/cli/commands/connect.ts +8 -4
  3. package/cli/commands/create.ts +106 -67
  4. package/cli/commands/deps.ts +19 -4
  5. package/cli/commands/edit.ts +245 -0
  6. package/cli/commands/engines.ts +434 -0
  7. package/cli/commands/info.ts +279 -0
  8. package/cli/commands/menu.ts +408 -153
  9. package/cli/commands/restore.ts +10 -24
  10. package/cli/commands/start.ts +25 -20
  11. package/cli/commands/url.ts +79 -0
  12. package/cli/index.ts +9 -3
  13. package/cli/ui/prompts.ts +8 -6
  14. package/config/engine-defaults.ts +24 -1
  15. package/config/os-dependencies.ts +59 -113
  16. package/config/paths.ts +7 -36
  17. package/core/binary-manager.ts +19 -6
  18. package/core/config-manager.ts +17 -5
  19. package/core/dependency-manager.ts +9 -15
  20. package/core/error-handler.ts +336 -0
  21. package/core/platform-service.ts +634 -0
  22. package/core/port-manager.ts +11 -3
  23. package/core/process-manager.ts +12 -2
  24. package/core/start-with-retry.ts +167 -0
  25. package/core/transaction-manager.ts +170 -0
  26. package/engines/mysql/binary-detection.ts +177 -100
  27. package/engines/mysql/index.ts +240 -131
  28. package/engines/mysql/restore.ts +257 -0
  29. package/engines/mysql/version-validator.ts +373 -0
  30. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  31. package/engines/postgresql/binary-urls.ts +5 -3
  32. package/engines/postgresql/index.ts +4 -3
  33. package/engines/postgresql/restore.ts +54 -5
  34. package/engines/postgresql/version-validator.ts +262 -0
  35. package/package.json +6 -2
  36. package/cli/commands/postgres-tools.ts +0 -216
@@ -12,10 +12,10 @@ import {
12
12
  } from '../ui/prompts'
13
13
  import { createSpinner } from '../ui/spinner'
14
14
  import { success, error, warning } from '../ui/theme'
15
- import { platform, tmpdir } from 'os'
16
- import { spawn } from 'child_process'
15
+ import { tmpdir } from 'os'
17
16
  import { join } from 'path'
18
17
  import { getMissingDependencies } from '../../core/dependency-manager'
18
+ import { platformService } from '../../core/platform-service'
19
19
 
20
20
  export const restoreCommand = new Command('restore')
21
21
  .description('Restore a backup to a container')
@@ -182,7 +182,10 @@ export const restoreCommand = new Command('restore')
182
182
  dumpSpinner.start()
183
183
 
184
184
  try {
185
- await engine.dumpFromConnectionString(options.fromUrl, tempDumpPath)
185
+ await engine.dumpFromConnectionString(
186
+ options.fromUrl,
187
+ tempDumpPath,
188
+ )
186
189
  dumpSpinner.succeed('Dump created from remote database')
187
190
  backupPath = tempDumpPath
188
191
  dumpSuccess = true
@@ -307,28 +310,11 @@ export const restoreCommand = new Command('restore')
307
310
  console.log(chalk.gray(' Connection string:'))
308
311
  console.log(chalk.cyan(` ${connectionString}`))
309
312
 
310
- // Copy connection string to clipboard using platform-specific command
311
- try {
312
- const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
313
- const args =
314
- platform() === 'darwin' ? [] : ['-selection', 'clipboard']
315
-
316
- await new Promise<void>((resolve, reject) => {
317
- const proc = spawn(cmd, args, {
318
- stdio: ['pipe', 'inherit', 'inherit'],
319
- })
320
- proc.stdin?.write(connectionString)
321
- proc.stdin?.end()
322
- proc.on('close', (code) => {
323
- if (code === 0) resolve()
324
- else
325
- reject(new Error(`Clipboard command exited with code ${code}`))
326
- })
327
- proc.on('error', reject)
328
- })
329
-
313
+ // Copy connection string to clipboard using platform service
314
+ const copied = await platformService.copyToClipboard(connectionString)
315
+ if (copied) {
330
316
  console.log(chalk.gray(' Connection string copied to clipboard'))
331
- } catch {
317
+ } else {
332
318
  console.log(chalk.gray(' (Could not copy to clipboard)'))
333
319
  }
334
320
 
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import { containerManager } from '../../core/container-manager'
4
- import { portManager } from '../../core/port-manager'
5
4
  import { processManager } from '../../core/process-manager'
5
+ import { startWithRetry } from '../../core/start-with-retry'
6
6
  import { getEngine } from '../../engines'
7
7
  import { getEngineDefaults } from '../../config/defaults'
8
8
  import { promptContainerSelect } from '../ui/prompts'
@@ -61,32 +61,37 @@ export const startCommand = new Command('start')
61
61
  // Get engine defaults for port range and database name
62
62
  const engineDefaults = getEngineDefaults(engineName)
63
63
 
64
- // Check port availability
65
- const portAvailable = await portManager.isPortAvailable(config.port)
66
- if (!portAvailable) {
67
- // Try to find a new port (using engine-specific port range)
68
- const { port: newPort } = await portManager.findAvailablePort({
69
- portRange: engineDefaults.portRange,
70
- })
71
- console.log(
72
- warning(
73
- `Port ${config.port} is in use, switching to port ${newPort}`,
74
- ),
75
- )
76
- config.port = newPort
77
- await containerManager.updateConfig(containerName, { port: newPort })
78
- }
79
-
80
- // Get engine and start
64
+ // Get engine and start with retry (handles port race conditions)
81
65
  const engine = getEngine(engineName)
82
66
 
83
67
  const spinner = createSpinner(`Starting ${containerName}...`)
84
68
  spinner.start()
85
69
 
86
- await engine.start(config)
70
+ const result = await startWithRetry({
71
+ engine,
72
+ config,
73
+ onPortChange: (oldPort, newPort) => {
74
+ spinner.text = `Port ${oldPort} was in use, retrying with port ${newPort}...`
75
+ },
76
+ })
77
+
78
+ if (!result.success) {
79
+ spinner.fail(`Failed to start "${containerName}"`)
80
+ if (result.error) {
81
+ console.error(error(result.error.message))
82
+ }
83
+ process.exit(1)
84
+ }
85
+
87
86
  await containerManager.updateConfig(containerName, { status: 'running' })
88
87
 
89
- spinner.succeed(`Container "${containerName}" started`)
88
+ if (result.retriesUsed > 0) {
89
+ spinner.warn(
90
+ `Container "${containerName}" started on port ${result.finalPort} (original port was in use)`,
91
+ )
92
+ } else {
93
+ spinner.succeed(`Container "${containerName}" started`)
94
+ }
90
95
 
91
96
  // Ensure the user's database exists (if different from default)
92
97
  const defaultDb = engineDefaults.superuser // postgres or root
@@ -0,0 +1,79 @@
1
+ import { Command } from 'commander'
2
+ import { containerManager } from '../../core/container-manager'
3
+ import { platformService } from '../../core/platform-service'
4
+ import { getEngine } from '../../engines'
5
+ import { promptContainerSelect } from '../ui/prompts'
6
+ import { error, warning, success } from '../ui/theme'
7
+
8
+ export const urlCommand = new Command('url')
9
+ .alias('connection-string')
10
+ .description('Output connection string for a container')
11
+ .argument('[name]', 'Container name')
12
+ .option('-c, --copy', 'Copy to clipboard')
13
+ .option('-d, --database <database>', 'Use different database name')
14
+ .action(
15
+ async (
16
+ name: string | undefined,
17
+ options: { copy?: boolean; database?: string },
18
+ ) => {
19
+ try {
20
+ let containerName = name
21
+
22
+ // Interactive selection if no name provided
23
+ if (!containerName) {
24
+ const containers = await containerManager.list()
25
+
26
+ if (containers.length === 0) {
27
+ console.log(warning('No containers found'))
28
+ return
29
+ }
30
+
31
+ const selected = await promptContainerSelect(
32
+ containers,
33
+ 'Select container:',
34
+ )
35
+ if (!selected) return
36
+ containerName = selected
37
+ }
38
+
39
+ // Get container config
40
+ const config = await containerManager.getConfig(containerName)
41
+ if (!config) {
42
+ console.error(error(`Container "${containerName}" not found`))
43
+ process.exit(1)
44
+ }
45
+
46
+ // Get connection string
47
+ const engine = getEngine(config.engine)
48
+ const connectionString = engine.getConnectionString(
49
+ config,
50
+ options.database,
51
+ )
52
+
53
+ // Copy to clipboard if requested
54
+ if (options.copy) {
55
+ const copied = await platformService.copyToClipboard(connectionString)
56
+ if (copied) {
57
+ // Output the string AND confirmation
58
+ console.log(connectionString)
59
+ console.error(success('Copied to clipboard'))
60
+ } else {
61
+ // Output the string but warn about clipboard
62
+ console.log(connectionString)
63
+ console.error(warning('Could not copy to clipboard'))
64
+ }
65
+ } else {
66
+ // Just output the connection string (no newline formatting for easy piping)
67
+ process.stdout.write(connectionString)
68
+ // Add newline if stdout is a TTY (interactive terminal)
69
+ if (process.stdout.isTTY) {
70
+ console.log()
71
+ }
72
+ }
73
+ } catch (err) {
74
+ const e = err as Error
75
+ console.error(error(e.message))
76
+ process.exit(1)
77
+ }
78
+ },
79
+ )
package/cli/index.ts CHANGED
@@ -9,14 +9,17 @@ import { connectCommand } from './commands/connect'
9
9
  import { cloneCommand } from './commands/clone'
10
10
  import { menuCommand } from './commands/menu'
11
11
  import { configCommand } from './commands/config'
12
- import { postgresToolsCommand } from './commands/postgres-tools'
13
12
  import { depsCommand } from './commands/deps'
13
+ import { enginesCommand } from './commands/engines'
14
+ import { editCommand } from './commands/edit'
15
+ import { urlCommand } from './commands/url'
16
+ import { infoCommand } from './commands/info'
14
17
 
15
18
  export async function run(): Promise<void> {
16
19
  program
17
20
  .name('spindb')
18
21
  .description('Spin up local database containers without Docker')
19
- .version('0.1.0')
22
+ .version('0.1.0', '-v, --version', 'output the version number')
20
23
 
21
24
  program.addCommand(createCommand)
22
25
  program.addCommand(listCommand)
@@ -28,8 +31,11 @@ export async function run(): Promise<void> {
28
31
  program.addCommand(cloneCommand)
29
32
  program.addCommand(menuCommand)
30
33
  program.addCommand(configCommand)
31
- program.addCommand(postgresToolsCommand)
32
34
  program.addCommand(depsCommand)
35
+ program.addCommand(enginesCommand)
36
+ program.addCommand(editCommand)
37
+ program.addCommand(urlCommand)
38
+ program.addCommand(infoCommand)
33
39
 
34
40
  // If no arguments provided, show interactive menu
35
41
  if (process.argv.length <= 2) {
package/cli/ui/prompts.ts CHANGED
@@ -3,7 +3,7 @@ import chalk from 'chalk'
3
3
  import ora from 'ora'
4
4
  import { listEngines, getEngine } from '../../engines'
5
5
  import { defaults, getEngineDefaults } from '../../config/defaults'
6
- import { installPostgresBinaries } from '../../core/postgres-binary-manager'
6
+ import { installPostgresBinaries } from '../../engines/postgresql/binary-manager'
7
7
  import {
8
8
  detectPackageManager,
9
9
  getManualInstallInstructions,
@@ -333,7 +333,9 @@ export async function promptInstallDependencies(
333
333
  if (dep) {
334
334
  const instructions = getManualInstallInstructions(dep, platform)
335
335
  console.log(
336
- chalk.gray(` Please install ${engineDeps.displayName} client tools:`),
336
+ chalk.gray(
337
+ ` Please install ${engineDeps.displayName} client tools:`,
338
+ ),
337
339
  )
338
340
  console.log()
339
341
  for (const instruction of instructions) {
@@ -346,7 +348,9 @@ export async function promptInstallDependencies(
346
348
  }
347
349
 
348
350
  console.log(
349
- chalk.gray(` Detected package manager: ${chalk.white(packageManager.name)}`),
351
+ chalk.gray(
352
+ ` Detected package manager: ${chalk.white(packageManager.name)}`,
353
+ ),
350
354
  )
351
355
  console.log()
352
356
 
@@ -425,9 +429,7 @@ export async function promptInstallDependencies(
425
429
 
426
430
  if (allSuccess) {
427
431
  console.log()
428
- console.log(
429
- chalk.green(` ${engineName} tools installed successfully!`),
430
- )
432
+ console.log(chalk.green(` ${engineName} tools installed successfully!`))
431
433
  console.log(chalk.gray(' Continuing with your operation...'))
432
434
  console.log()
433
435
  return true
@@ -12,6 +12,8 @@ export type EngineDefaults = {
12
12
  portRange: { start: number; end: number }
13
13
  /** Supported major versions */
14
14
  supportedVersions: string[]
15
+ /** Latest major version (used for Homebrew package names like postgresql@17) */
16
+ latestVersion: string
15
17
  /** Default superuser name */
16
18
  superuser: string
17
19
  /** Connection string scheme (e.g., 'postgresql', 'mysql') */
@@ -28,10 +30,11 @@ export type EngineDefaults = {
28
30
 
29
31
  export const engineDefaults: Record<string, EngineDefaults> = {
30
32
  postgresql: {
31
- defaultVersion: '16',
33
+ defaultVersion: '17',
32
34
  defaultPort: 5432,
33
35
  portRange: { start: 5432, end: 5500 },
34
36
  supportedVersions: ['14', '15', '16', '17'],
37
+ latestVersion: '17', // Update when PostgreSQL 18 is released
35
38
  superuser: 'postgres',
36
39
  connectionScheme: 'postgresql',
37
40
  logFileName: 'postgres.log',
@@ -44,6 +47,7 @@ export const engineDefaults: Record<string, EngineDefaults> = {
44
47
  defaultPort: 3306,
45
48
  portRange: { start: 3306, end: 3400 },
46
49
  supportedVersions: ['5.7', '8.0', '8.4', '9.0'],
50
+ latestVersion: '9.0', // MySQL doesn't use versioned Homebrew packages, but kept for consistency
47
51
  superuser: 'root',
48
52
  connectionScheme: 'mysql',
49
53
  logFileName: 'mysql.log',
@@ -82,3 +86,22 @@ export function isEngineSupported(engine: string): boolean {
82
86
  export function getSupportedEngines(): string[] {
83
87
  return Object.keys(engineDefaults)
84
88
  }
89
+
90
+ /**
91
+ * Get Homebrew package name for PostgreSQL client tools
92
+ * Returns 'postgresql@17' format for versioned installs
93
+ */
94
+ export function getPostgresHomebrewPackage(): string {
95
+ const version = engineDefaults.postgresql.latestVersion
96
+ return `postgresql@${version}`
97
+ }
98
+
99
+ /**
100
+ * Get the PostgreSQL Homebrew bin path for a given architecture
101
+ * @param arch - 'arm64' or 'x64'
102
+ */
103
+ export function getPostgresHomebrewBinPath(arch: 'arm64' | 'x64'): string {
104
+ const pkg = getPostgresHomebrewPackage()
105
+ const prefix = arch === 'arm64' ? '/opt/homebrew' : '/usr/local'
106
+ return `${prefix}/opt/${pkg}/bin`
107
+ }
@@ -5,9 +5,11 @@
5
5
  * across different operating systems and package managers.
6
6
  */
7
7
 
8
+ import { getPostgresHomebrewPackage } from './engine-defaults'
9
+
8
10
  export type PackageManagerId = 'brew' | 'apt' | 'yum' | 'dnf' | 'pacman'
9
11
 
10
- export type Platform = 'darwin' | 'linux'
12
+ export type Platform = 'darwin' | 'linux' | 'win32'
11
13
 
12
14
  /**
13
15
  * Package definition for a specific package manager
@@ -116,122 +118,66 @@ export const packageManagers: PackageManagerConfig[] = [
116
118
  // PostgreSQL Dependencies
117
119
  // =============================================================================
118
120
 
121
+ /**
122
+ * Helper to create PostgreSQL client tool dependency
123
+ * Uses getPostgresHomebrewPackage() to get the current latest version
124
+ */
125
+ function createPostgresDependency(
126
+ name: string,
127
+ binary: string,
128
+ description: string,
129
+ ): Dependency {
130
+ const pgPackage = getPostgresHomebrewPackage()
131
+ return {
132
+ name,
133
+ binary,
134
+ description,
135
+ packages: {
136
+ brew: {
137
+ package: pgPackage,
138
+ postInstall: [`brew link --overwrite ${pgPackage}`],
139
+ },
140
+ apt: { package: 'postgresql-client' },
141
+ yum: { package: 'postgresql' },
142
+ dnf: { package: 'postgresql' },
143
+ pacman: { package: 'postgresql-libs' },
144
+ },
145
+ manualInstall: {
146
+ darwin: [
147
+ 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
148
+ `Then run: brew install ${pgPackage} && brew link --overwrite ${pgPackage}`,
149
+ 'Or install Postgres.app: https://postgresapp.com/downloads.html',
150
+ ],
151
+ linux: [
152
+ 'Ubuntu/Debian: sudo apt install postgresql-client',
153
+ 'CentOS/RHEL: sudo yum install postgresql',
154
+ 'Fedora: sudo dnf install postgresql',
155
+ 'Arch: sudo pacman -S postgresql-libs',
156
+ ],
157
+ },
158
+ }
159
+ }
160
+
119
161
  const postgresqlDependencies: EngineDependencies = {
120
162
  engine: 'postgresql',
121
163
  displayName: 'PostgreSQL',
122
164
  dependencies: [
123
- {
124
- name: 'psql',
125
- binary: 'psql',
126
- description: 'PostgreSQL interactive terminal',
127
- packages: {
128
- brew: {
129
- package: 'postgresql@17',
130
- postInstall: ['brew link --overwrite postgresql@17'],
131
- },
132
- apt: { package: 'postgresql-client' },
133
- yum: { package: 'postgresql' },
134
- dnf: { package: 'postgresql' },
135
- pacman: { package: 'postgresql-libs' },
136
- },
137
- manualInstall: {
138
- darwin: [
139
- 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
140
- 'Then run: brew install postgresql@17 && brew link --overwrite postgresql@17',
141
- 'Or install Postgres.app: https://postgresapp.com/downloads.html',
142
- ],
143
- linux: [
144
- 'Ubuntu/Debian: sudo apt install postgresql-client',
145
- 'CentOS/RHEL: sudo yum install postgresql',
146
- 'Fedora: sudo dnf install postgresql',
147
- 'Arch: sudo pacman -S postgresql-libs',
148
- ],
149
- },
150
- },
151
- {
152
- name: 'pg_dump',
153
- binary: 'pg_dump',
154
- description: 'PostgreSQL database backup utility',
155
- packages: {
156
- brew: {
157
- package: 'postgresql@17',
158
- postInstall: ['brew link --overwrite postgresql@17'],
159
- },
160
- apt: { package: 'postgresql-client' },
161
- yum: { package: 'postgresql' },
162
- dnf: { package: 'postgresql' },
163
- pacman: { package: 'postgresql-libs' },
164
- },
165
- manualInstall: {
166
- darwin: [
167
- 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
168
- 'Then run: brew install postgresql@17 && brew link --overwrite postgresql@17',
169
- 'Or install Postgres.app: https://postgresapp.com/downloads.html',
170
- ],
171
- linux: [
172
- 'Ubuntu/Debian: sudo apt install postgresql-client',
173
- 'CentOS/RHEL: sudo yum install postgresql',
174
- 'Fedora: sudo dnf install postgresql',
175
- 'Arch: sudo pacman -S postgresql-libs',
176
- ],
177
- },
178
- },
179
- {
180
- name: 'pg_restore',
181
- binary: 'pg_restore',
182
- description: 'PostgreSQL database restore utility',
183
- packages: {
184
- brew: {
185
- package: 'postgresql@17',
186
- postInstall: ['brew link --overwrite postgresql@17'],
187
- },
188
- apt: { package: 'postgresql-client' },
189
- yum: { package: 'postgresql' },
190
- dnf: { package: 'postgresql' },
191
- pacman: { package: 'postgresql-libs' },
192
- },
193
- manualInstall: {
194
- darwin: [
195
- 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
196
- 'Then run: brew install postgresql@17 && brew link --overwrite postgresql@17',
197
- 'Or install Postgres.app: https://postgresapp.com/downloads.html',
198
- ],
199
- linux: [
200
- 'Ubuntu/Debian: sudo apt install postgresql-client',
201
- 'CentOS/RHEL: sudo yum install postgresql',
202
- 'Fedora: sudo dnf install postgresql',
203
- 'Arch: sudo pacman -S postgresql-libs',
204
- ],
205
- },
206
- },
207
- {
208
- name: 'pg_basebackup',
209
- binary: 'pg_basebackup',
210
- description: 'PostgreSQL base backup utility for physical backups',
211
- packages: {
212
- brew: {
213
- package: 'postgresql@17',
214
- postInstall: ['brew link --overwrite postgresql@17'],
215
- },
216
- apt: { package: 'postgresql-client' },
217
- yum: { package: 'postgresql' },
218
- dnf: { package: 'postgresql' },
219
- pacman: { package: 'postgresql-libs' },
220
- },
221
- manualInstall: {
222
- darwin: [
223
- 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
224
- 'Then run: brew install postgresql@17 && brew link --overwrite postgresql@17',
225
- 'Or install Postgres.app: https://postgresapp.com/downloads.html',
226
- ],
227
- linux: [
228
- 'Ubuntu/Debian: sudo apt install postgresql-client',
229
- 'CentOS/RHEL: sudo yum install postgresql',
230
- 'Fedora: sudo dnf install postgresql',
231
- 'Arch: sudo pacman -S postgresql-libs',
232
- ],
233
- },
234
- },
165
+ createPostgresDependency('psql', 'psql', 'PostgreSQL interactive terminal'),
166
+ createPostgresDependency(
167
+ 'pg_dump',
168
+ 'pg_dump',
169
+ 'PostgreSQL database backup utility',
170
+ ),
171
+ createPostgresDependency(
172
+ 'pg_restore',
173
+ 'pg_restore',
174
+ 'PostgreSQL database restore utility',
175
+ ),
176
+ createPostgresDependency(
177
+ 'pg_basebackup',
178
+ 'pg_basebackup',
179
+ 'PostgreSQL base backup utility for physical backups',
180
+ ),
235
181
  ],
236
182
  }
237
183
 
package/config/paths.ts CHANGED
@@ -1,46 +1,17 @@
1
- import { homedir } from 'os'
2
1
  import { join } from 'path'
3
- import { execSync } from 'child_process'
4
2
  import { getEngineDefaults } from './engine-defaults'
3
+ import { platformService } from '../core/platform-service'
5
4
 
6
5
  /**
7
- * Get the real user's home directory, even when running under sudo.
8
- * When a user runs `sudo spindb`, we want to use their home directory,
9
- * not root's home directory.
6
+ * Get the SpinDB home directory using the platform service.
7
+ * This handles sudo detection and platform-specific home directories.
10
8
  */
11
- function getRealHomeDir(): string {
12
- // Check if running under sudo
13
- const sudoUser = process.env.SUDO_USER
14
-
15
- if (sudoUser) {
16
- // Get the original user's home directory
17
- try {
18
- // Use getent to reliably get the home directory for the sudo user
19
- const result = execSync(`getent passwd ${sudoUser}`, {
20
- encoding: 'utf-8',
21
- })
22
- const parts = result.trim().split(':')
23
- if (parts.length >= 6 && parts[5]) {
24
- return parts[5]
25
- }
26
- } catch {
27
- // Fall back to constructing the path
28
- // On most Linux systems, home dirs are /home/username
29
- // On macOS, they're /Users/username
30
- const platform = process.platform
31
- if (platform === 'darwin') {
32
- return `/Users/${sudoUser}`
33
- } else {
34
- return `/home/${sudoUser}`
35
- }
36
- }
37
- }
38
-
39
- // Not running under sudo, use normal homedir
40
- return homedir()
9
+ function getSpinDBHome(): string {
10
+ const platformInfo = platformService.getPlatformInfo()
11
+ return join(platformInfo.homeDir, '.spindb')
41
12
  }
42
13
 
43
- const SPINDB_HOME = join(getRealHomeDir(), '.spindb')
14
+ const SPINDB_HOME = getSpinDBHome()
44
15
 
45
16
  /**
46
17
  * Options for container path functions
@@ -54,6 +54,14 @@ 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
+
57
65
  /**
58
66
  * Check if binaries for a specific version are already installed
59
67
  */
@@ -62,9 +70,10 @@ export class BinaryManager {
62
70
  platform: string,
63
71
  arch: string,
64
72
  ): Promise<boolean> {
73
+ const majorVersion = this.getMajorVersion(version)
65
74
  const binPath = paths.getBinaryPath({
66
75
  engine: 'postgresql',
67
- version,
76
+ version: majorVersion,
68
77
  platform,
69
78
  arch,
70
79
  })
@@ -113,14 +122,15 @@ export class BinaryManager {
113
122
  arch: string,
114
123
  onProgress?: ProgressCallback,
115
124
  ): Promise<string> {
125
+ const majorVersion = this.getMajorVersion(version)
116
126
  const url = this.getDownloadUrl(version, platform, arch)
117
127
  const binPath = paths.getBinaryPath({
118
128
  engine: 'postgresql',
119
- version,
129
+ version: majorVersion,
120
130
  platform,
121
131
  arch,
122
132
  })
123
- const tempDir = join(paths.bin, `temp-${version}-${platform}-${arch}`)
133
+ const tempDir = join(paths.bin, `temp-${majorVersion}-${platform}-${arch}`)
124
134
  const jarFile = join(tempDir, 'postgres.jar')
125
135
 
126
136
  // Ensure directories exist
@@ -200,9 +210,10 @@ export class BinaryManager {
200
210
  platform: string,
201
211
  arch: string,
202
212
  ): Promise<boolean> {
213
+ const majorVersion = this.getMajorVersion(version)
203
214
  const binPath = paths.getBinaryPath({
204
215
  engine: 'postgresql',
205
- version,
216
+ version: majorVersion,
206
217
  platform,
207
218
  arch,
208
219
  })
@@ -256,9 +267,10 @@ export class BinaryManager {
256
267
  arch: string,
257
268
  binary: string,
258
269
  ): string {
270
+ const majorVersion = this.getMajorVersion(version)
259
271
  const binPath = paths.getBinaryPath({
260
272
  engine: 'postgresql',
261
- version,
273
+ version: majorVersion,
262
274
  platform,
263
275
  arch,
264
276
  })
@@ -274,6 +286,7 @@ export class BinaryManager {
274
286
  arch: string,
275
287
  onProgress?: ProgressCallback,
276
288
  ): Promise<string> {
289
+ const majorVersion = this.getMajorVersion(version)
277
290
  if (await this.isInstalled(version, platform, arch)) {
278
291
  onProgress?.({
279
292
  stage: 'cached',
@@ -281,7 +294,7 @@ export class BinaryManager {
281
294
  })
282
295
  return paths.getBinaryPath({
283
296
  engine: 'postgresql',
284
- version,
297
+ version: majorVersion,
285
298
  platform,
286
299
  arch,
287
300
  })