spindb 0.9.3 → 0.10.0

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.
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { getPostgresHomebrewPackage } from './engine-defaults'
9
9
 
10
- export type PackageManagerId = 'brew' | 'apt' | 'yum' | 'dnf' | 'pacman'
10
+ export type PackageManagerId = 'brew' | 'apt' | 'yum' | 'dnf' | 'pacman' | 'choco' | 'winget' | 'scoop'
11
11
 
12
12
  export type Platform = 'darwin' | 'linux' | 'win32'
13
13
 
@@ -112,6 +112,30 @@ export const packageManagers: PackageManagerConfig[] = [
112
112
  installTemplate: 'sudo pacman -S --noconfirm {package}',
113
113
  updateTemplate: 'sudo pacman -Syu --noconfirm {package}',
114
114
  },
115
+ {
116
+ id: 'choco',
117
+ name: 'Chocolatey',
118
+ checkCommand: 'choco --version',
119
+ platforms: ['win32'],
120
+ installTemplate: 'choco install -y {package}',
121
+ updateTemplate: 'choco upgrade -y {package}',
122
+ },
123
+ {
124
+ id: 'winget',
125
+ name: 'Windows Package Manager',
126
+ checkCommand: 'winget --version',
127
+ platforms: ['win32'],
128
+ installTemplate: 'winget install {package}',
129
+ updateTemplate: 'winget upgrade {package}',
130
+ },
131
+ {
132
+ id: 'scoop',
133
+ name: 'Scoop',
134
+ checkCommand: 'scoop --version',
135
+ platforms: ['win32'],
136
+ installTemplate: 'scoop install {package}',
137
+ updateTemplate: 'scoop update {package}',
138
+ },
115
139
  ]
116
140
 
117
141
  // =============================================================================
@@ -141,6 +165,9 @@ function createPostgresDependency(
141
165
  yum: { package: 'postgresql' },
142
166
  dnf: { package: 'postgresql' },
143
167
  pacman: { package: 'postgresql-libs' },
168
+ choco: { package: 'postgresql' },
169
+ winget: { package: 'PostgreSQL.PostgreSQL' },
170
+ scoop: { package: 'postgresql' },
144
171
  },
145
172
  manualInstall: {
146
173
  darwin: [
@@ -154,6 +181,12 @@ function createPostgresDependency(
154
181
  'Fedora: sudo dnf install postgresql',
155
182
  'Arch: sudo pacman -S postgresql-libs',
156
183
  ],
184
+ win32: [
185
+ 'Using Chocolatey: choco install postgresql',
186
+ 'Using winget: winget install PostgreSQL.PostgreSQL',
187
+ 'Using Scoop: scoop install postgresql',
188
+ 'Or download from: https://www.enterprisedb.com/downloads/postgres-postgresql-downloads',
189
+ ],
157
190
  },
158
191
  }
159
192
  }
@@ -197,6 +230,9 @@ const mysqlDependencies: EngineDependencies = {
197
230
  brew: { package: 'mysql' },
198
231
  // Modern Debian/Ubuntu use mariadb-server (MySQL-compatible)
199
232
  apt: { package: 'mariadb-server' },
233
+ choco: { package: 'mysql' },
234
+ winget: { package: 'Oracle.MySQL' },
235
+ scoop: { package: 'mysql' },
200
236
  yum: { package: 'mariadb-server' },
201
237
  dnf: { package: 'mariadb-server' },
202
238
  pacman: { package: 'mariadb' },
@@ -212,6 +248,12 @@ const mysqlDependencies: EngineDependencies = {
212
248
  'Fedora: sudo dnf install mariadb-server',
213
249
  'Arch: sudo pacman -S mariadb',
214
250
  ],
251
+ win32: [
252
+ 'Using Chocolatey: choco install mysql',
253
+ 'Using winget: winget install Oracle.MySQL',
254
+ 'Using Scoop: scoop install mysql',
255
+ 'Or download from: https://dev.mysql.com/downloads/mysql/',
256
+ ],
215
257
  },
216
258
  },
217
259
  {
@@ -224,6 +266,9 @@ const mysqlDependencies: EngineDependencies = {
224
266
  yum: { package: 'mariadb' },
225
267
  dnf: { package: 'mariadb' },
226
268
  pacman: { package: 'mariadb-clients' },
269
+ choco: { package: 'mysql' },
270
+ winget: { package: 'Oracle.MySQL' },
271
+ scoop: { package: 'mysql' },
227
272
  },
228
273
  manualInstall: {
229
274
  darwin: [
@@ -236,6 +281,12 @@ const mysqlDependencies: EngineDependencies = {
236
281
  'Fedora: sudo dnf install mariadb',
237
282
  'Arch: sudo pacman -S mariadb-clients',
238
283
  ],
284
+ win32: [
285
+ 'Using Chocolatey: choco install mysql',
286
+ 'Using winget: winget install Oracle.MySQL',
287
+ 'Using Scoop: scoop install mysql',
288
+ 'Or download from: https://dev.mysql.com/downloads/mysql/',
289
+ ],
239
290
  },
240
291
  },
241
292
  {
@@ -248,6 +299,9 @@ const mysqlDependencies: EngineDependencies = {
248
299
  yum: { package: 'mariadb' },
249
300
  dnf: { package: 'mariadb' },
250
301
  pacman: { package: 'mariadb-clients' },
302
+ choco: { package: 'mysql' },
303
+ winget: { package: 'Oracle.MySQL' },
304
+ scoop: { package: 'mysql' },
251
305
  },
252
306
  manualInstall: {
253
307
  darwin: [
@@ -260,6 +314,12 @@ const mysqlDependencies: EngineDependencies = {
260
314
  'Fedora: sudo dnf install mariadb',
261
315
  'Arch: sudo pacman -S mariadb-clients',
262
316
  ],
317
+ win32: [
318
+ 'Using Chocolatey: choco install mysql',
319
+ 'Using winget: winget install Oracle.MySQL',
320
+ 'Using Scoop: scoop install mysql',
321
+ 'Or download from: https://dev.mysql.com/downloads/mysql/',
322
+ ],
263
323
  },
264
324
  },
265
325
  {
@@ -272,6 +332,9 @@ const mysqlDependencies: EngineDependencies = {
272
332
  yum: { package: 'mariadb' },
273
333
  dnf: { package: 'mariadb' },
274
334
  pacman: { package: 'mariadb-clients' },
335
+ choco: { package: 'mysql' },
336
+ winget: { package: 'Oracle.MySQL' },
337
+ scoop: { package: 'mysql' },
275
338
  },
276
339
  manualInstall: {
277
340
  darwin: [
@@ -284,6 +347,12 @@ const mysqlDependencies: EngineDependencies = {
284
347
  'Fedora: sudo dnf install mariadb',
285
348
  'Arch: sudo pacman -S mariadb-clients',
286
349
  ],
350
+ win32: [
351
+ 'Using Chocolatey: choco install mysql',
352
+ 'Using winget: winget install Oracle.MySQL',
353
+ 'Using Scoop: scoop install mysql',
354
+ 'Or download from: https://dev.mysql.com/downloads/mysql/',
355
+ ],
287
356
  },
288
357
  },
289
358
  ],
@@ -307,6 +376,9 @@ const sqliteDependencies: EngineDependencies = {
307
376
  yum: { package: 'sqlite' },
308
377
  dnf: { package: 'sqlite' },
309
378
  pacman: { package: 'sqlite' },
379
+ choco: { package: 'sqlite' },
380
+ winget: { package: 'SQLite.SQLite' },
381
+ scoop: { package: 'sqlite' },
310
382
  },
311
383
  manualInstall: {
312
384
  darwin: [
@@ -320,6 +392,12 @@ const sqliteDependencies: EngineDependencies = {
320
392
  'Fedora: sudo dnf install sqlite',
321
393
  'Arch: sudo pacman -S sqlite',
322
394
  ],
395
+ win32: [
396
+ 'Using Chocolatey: choco install sqlite',
397
+ 'Using winget: winget install SQLite.SQLite',
398
+ 'Using Scoop: scoop install sqlite',
399
+ 'Or download from: https://www.sqlite.org/download.html',
400
+ ],
323
401
  },
324
402
  },
325
403
  ],
@@ -1,11 +1,14 @@
1
- import { createWriteStream, existsSync } from 'fs'
2
- import { mkdir, readdir, rm, chmod } from 'fs/promises'
1
+ import { createWriteStream, existsSync, createReadStream } from 'fs'
2
+ import { mkdir, readdir, rm, chmod, rename, cp } from 'fs/promises'
3
3
  import { join } from 'path'
4
4
  import { pipeline } from 'stream/promises'
5
5
  import { exec } from 'child_process'
6
6
  import { promisify } from 'util'
7
+ import unzipper from 'unzipper'
7
8
  import { paths } from '../config/paths'
8
9
  import { defaults } from '../config/defaults'
10
+ import { getEDBBinaryUrl } from '../engines/postgresql/edb-binary-urls'
11
+ import { normalizeVersion } from '../engines/postgresql/version-maps'
9
12
  import {
10
13
  type Engine,
11
14
  type ProgressCallback,
@@ -17,9 +20,24 @@ const execAsync = promisify(exec)
17
20
  export class BinaryManager {
18
21
  /**
19
22
  * Get the download URL for a PostgreSQL version
23
+ *
24
+ * - macOS/Linux: Uses zonky.io Maven Central binaries (JAR format)
25
+ * - Windows: Uses EDB (EnterpriseDB) official binaries (ZIP format)
20
26
  */
21
27
  getDownloadUrl(version: string, platform: string, arch: string): string {
22
28
  const platformKey = `${platform}-${arch}`
29
+
30
+ if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
31
+ throw new Error(`Unsupported platform: ${platformKey}`)
32
+ }
33
+
34
+ // Windows uses EDB binaries instead of zonky.io
35
+ if (platform === 'win32') {
36
+ const fullVersion = this.getFullVersion(version)
37
+ return getEDBBinaryUrl(fullVersion)
38
+ }
39
+
40
+ // macOS/Linux use zonky.io binaries
23
41
  const zonkyPlatform = defaults.platformMappings[platformKey]
24
42
 
25
43
  if (!zonkyPlatform) {
@@ -32,30 +50,13 @@ export class BinaryManager {
32
50
  }
33
51
 
34
52
  /**
35
- * Convert version to full version format (e.g., "16" -> "16.6.0", "16.9" -> "16.9.0")
53
+ * Convert version to full version format (e.g., "16" -> "16.11.0", "16.9" -> "16.9.0")
54
+ *
55
+ * Uses the shared version mappings from version-maps.ts.
56
+ * Both zonky.io (macOS/Linux) and EDB (Windows) use the same PostgreSQL versions.
36
57
  */
37
58
  getFullVersion(version: string): string {
38
- // Map major versions to latest stable patch versions
39
- // Updated from: https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-darwin-arm64v8/
40
- const versionMap: Record<string, string> = {
41
- '14': '14.20.0',
42
- '15': '15.15.0',
43
- '16': '16.11.0',
44
- '17': '17.7.0',
45
- }
46
-
47
- // If it's a major version only, use the map
48
- if (versionMap[version]) {
49
- return versionMap[version]
50
- }
51
-
52
- // Normalize to X.Y.Z format
53
- const parts = version.split('.')
54
- if (parts.length === 2) {
55
- return `${version}.0`
56
- }
57
-
58
- return version
59
+ return normalizeVersion(version)
59
60
  }
60
61
 
61
62
  /**
@@ -74,7 +75,8 @@ export class BinaryManager {
74
75
  platform,
75
76
  arch,
76
77
  })
77
- const postgresPath = join(binPath, 'bin', 'postgres')
78
+ const ext = platform === 'win32' ? '.exe' : ''
79
+ const postgresPath = join(binPath, 'bin', `postgres${ext}`)
78
80
  return existsSync(postgresPath)
79
81
  }
80
82
 
@@ -110,8 +112,9 @@ export class BinaryManager {
110
112
  /**
111
113
  * Download and extract PostgreSQL binaries
112
114
  *
113
- * The zonky.io JAR files are ZIP archives containing a .txz (tar.xz) file.
114
- * We need to: 1) unzip the JAR, 2) extract the .txz inside
115
+ * - macOS/Linux (zonky.io): JAR files are ZIP archives containing a .txz (tar.xz) file.
116
+ * We need to: 1) unzip the JAR, 2) extract the .txz inside
117
+ * - Windows (EDB): ZIP files extract directly to a PostgreSQL directory structure
115
118
  */
116
119
  async download(
117
120
  version: string,
@@ -128,7 +131,10 @@ export class BinaryManager {
128
131
  arch,
129
132
  })
130
133
  const tempDir = join(paths.bin, `temp-${fullVersion}-${platform}-${arch}`)
131
- const jarFile = join(tempDir, 'postgres.jar')
134
+ const archiveFile = join(
135
+ tempDir,
136
+ platform === 'win32' ? 'postgres.zip' : 'postgres.jar',
137
+ )
132
138
 
133
139
  // Ensure directories exist
134
140
  await mkdir(paths.bin, { recursive: true })
@@ -136,7 +142,7 @@ export class BinaryManager {
136
142
  await mkdir(binPath, { recursive: true })
137
143
 
138
144
  try {
139
- // Download the JAR file
145
+ // Download the archive
140
146
  onProgress?.({
141
147
  stage: 'downloading',
142
148
  message: 'Downloading PostgreSQL binaries...',
@@ -149,42 +155,36 @@ export class BinaryManager {
149
155
  )
150
156
  }
151
157
 
152
- const fileStream = createWriteStream(jarFile)
158
+ const fileStream = createWriteStream(archiveFile)
153
159
  // @ts-expect-error - response.body is ReadableStream
154
160
  await pipeline(response.body, fileStream)
155
161
 
156
- // Extract the JAR (it's a ZIP file)
157
- onProgress?.({
158
- stage: 'extracting',
159
- message: 'Extracting binaries (step 1/2)...',
160
- })
161
-
162
- await execAsync(`unzip -q -o "${jarFile}" -d "${tempDir}"`)
163
-
164
- // Find and extract the .txz file inside
165
- onProgress?.({
166
- stage: 'extracting',
167
- message: 'Extracting binaries (step 2/2)...',
168
- })
169
-
170
- const { stdout: findOutput } = await execAsync(
171
- `find "${tempDir}" -name "*.txz" -o -name "*.tar.xz" | head -1`,
172
- )
173
- const txzFile = findOutput.trim()
174
-
175
- if (!txzFile) {
176
- throw new Error('Could not find .txz file in downloaded archive')
162
+ if (platform === 'win32') {
163
+ // Windows: EDB ZIP extracts directly to PostgreSQL structure
164
+ await this.extractWindowsBinaries(
165
+ archiveFile,
166
+ binPath,
167
+ tempDir,
168
+ onProgress,
169
+ )
170
+ } else {
171
+ // macOS/Linux: zonky.io JAR contains .txz that needs secondary extraction
172
+ await this.extractUnixBinaries(
173
+ archiveFile,
174
+ binPath,
175
+ tempDir,
176
+ onProgress,
177
+ )
177
178
  }
178
179
 
179
- // Extract the tar.xz file (no strip-components since files are at root level)
180
- await execAsync(`tar -xJf "${txzFile}" -C "${binPath}"`)
181
-
182
- // Make binaries executable
183
- const binDir = join(binPath, 'bin')
184
- if (existsSync(binDir)) {
185
- const binaries = await readdir(binDir)
186
- for (const binary of binaries) {
187
- await chmod(join(binDir, binary), 0o755)
180
+ // Make binaries executable (on Unix-like systems)
181
+ if (platform !== 'win32') {
182
+ const binDir = join(binPath, 'bin')
183
+ if (existsSync(binDir)) {
184
+ const binaries = await readdir(binDir)
185
+ for (const binary of binaries) {
186
+ await chmod(join(binDir, binary), 0o755)
187
+ }
188
188
  }
189
189
  }
190
190
 
@@ -199,6 +199,119 @@ export class BinaryManager {
199
199
  }
200
200
  }
201
201
 
202
+ /**
203
+ * Extract Windows binaries from EDB ZIP file
204
+ * EDB ZIPs contain a pgsql/ directory with bin/, lib/, share/ etc.
205
+ */
206
+ private async extractWindowsBinaries(
207
+ zipFile: string,
208
+ binPath: string,
209
+ tempDir: string,
210
+ onProgress?: ProgressCallback,
211
+ ): Promise<void> {
212
+ onProgress?.({
213
+ stage: 'extracting',
214
+ message: 'Extracting binaries...',
215
+ })
216
+
217
+ // Extract ZIP to temp directory first
218
+ await new Promise<void>((resolve, reject) => {
219
+ createReadStream(zipFile)
220
+ .pipe(unzipper.Extract({ path: tempDir }))
221
+ .on('close', resolve)
222
+ .on('error', reject)
223
+ })
224
+
225
+ // EDB ZIPs have a pgsql/ directory - find it and move contents to binPath
226
+ const entries = await readdir(tempDir, { withFileTypes: true })
227
+ const pgsqlDir = entries.find(
228
+ (e) =>
229
+ e.isDirectory() &&
230
+ (e.name === 'pgsql' || e.name.startsWith('postgresql-')),
231
+ )
232
+
233
+ if (pgsqlDir) {
234
+ // Move contents from pgsql/ to binPath using cross-platform Node.js fs methods
235
+ const sourceDir = join(tempDir, pgsqlDir.name)
236
+ const sourceEntries = await readdir(sourceDir, { withFileTypes: true })
237
+ for (const entry of sourceEntries) {
238
+ const sourcePath = join(sourceDir, entry.name)
239
+ const destPath = join(binPath, entry.name)
240
+ try {
241
+ // Try rename first (works if on same filesystem)
242
+ await rename(sourcePath, destPath)
243
+ } catch {
244
+ // Fallback to recursive copy for cross-filesystem moves
245
+ await cp(sourcePath, destPath, { recursive: true })
246
+ }
247
+ }
248
+ } else {
249
+ // No pgsql directory, extract contents directly
250
+ throw new Error(
251
+ 'Unexpected EDB archive structure - no pgsql directory found',
252
+ )
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Extract Unix binaries from zonky.io JAR file
258
+ * JAR contains a .txz (tar.xz) file that needs secondary extraction
259
+ */
260
+ private async extractUnixBinaries(
261
+ jarFile: string,
262
+ binPath: string,
263
+ tempDir: string,
264
+ onProgress?: ProgressCallback,
265
+ ): Promise<void> {
266
+ // Extract the JAR (it's a ZIP file) using unzipper
267
+ onProgress?.({
268
+ stage: 'extracting',
269
+ message: 'Extracting binaries (step 1/2)...',
270
+ })
271
+
272
+ await new Promise<void>((resolve, reject) => {
273
+ createReadStream(jarFile)
274
+ .pipe(unzipper.Extract({ path: tempDir }))
275
+ .on('close', resolve)
276
+ .on('error', reject)
277
+ })
278
+
279
+ // Find the .txz file inside
280
+ onProgress?.({
281
+ stage: 'extracting',
282
+ message: 'Extracting binaries (step 2/2)...',
283
+ })
284
+
285
+ const txzFile = await this.findTxzFile(tempDir)
286
+ if (!txzFile) {
287
+ throw new Error('Could not find .txz file in downloaded archive')
288
+ }
289
+
290
+ // Extract the tar.xz file (no strip-components since files are at root level)
291
+ await execAsync(`tar -xJf "${txzFile}" -C "${binPath}"`)
292
+ }
293
+
294
+ /**
295
+ * Recursively find a .txz or .tar.xz file in a directory
296
+ */
297
+ private async findTxzFile(dir: string): Promise<string | null> {
298
+ const entries = await readdir(dir, { withFileTypes: true })
299
+ for (const entry of entries) {
300
+ const fullPath = join(dir, entry.name)
301
+ if (
302
+ entry.isFile() &&
303
+ (entry.name.endsWith('.txz') || entry.name.endsWith('.tar.xz'))
304
+ ) {
305
+ return fullPath
306
+ }
307
+ if (entry.isDirectory()) {
308
+ const found = await this.findTxzFile(fullPath)
309
+ if (found) return found
310
+ }
311
+ }
312
+ return null
313
+ }
314
+
202
315
  /**
203
316
  * Verify that PostgreSQL binaries are working
204
317
  */
@@ -214,7 +327,8 @@ export class BinaryManager {
214
327
  platform,
215
328
  arch,
216
329
  })
217
- const postgresPath = join(binPath, 'bin', 'postgres')
330
+ const ext = platform === 'win32' ? '.exe' : ''
331
+ const postgresPath = join(binPath, 'bin', `postgres${ext}`)
218
332
 
219
333
  if (!existsSync(postgresPath)) {
220
334
  throw new Error(`PostgreSQL binary not found at ${postgresPath}`)
@@ -229,10 +343,10 @@ export class BinaryManager {
229
343
  }
230
344
 
231
345
  const reportedVersion = match[1]
232
- // Normalize both versions for comparison (16.9.0 -> 16.9, 16 -> 16)
233
- const normalizeVersion = (v: string) => v.replace(/\.0$/, '')
234
- const expectedNormalized = normalizeVersion(version)
235
- const reportedNormalized = normalizeVersion(reportedVersion)
346
+ // Strip trailing .0 for comparison (16.9.0 -> 16.9, 16 -> 16)
347
+ const stripTrailingZero = (v: string) => v.replace(/\.0$/, '')
348
+ const expectedNormalized = stripTrailingZero(version)
349
+ const reportedNormalized = stripTrailingZero(reportedVersion)
236
350
 
237
351
  // Check if versions match (after normalization)
238
352
  if (reportedNormalized === expectedNormalized) {
@@ -271,7 +385,8 @@ export class BinaryManager {
271
385
  platform,
272
386
  arch,
273
387
  })
274
- return join(binPath, 'bin', binary)
388
+ const ext = platform === 'win32' ? '.exe' : ''
389
+ return join(binPath, 'bin', `${binary}${ext}`)
275
390
  }
276
391
 
277
392
  /**
@@ -5,6 +5,7 @@ import { promisify } from 'util'
5
5
  import { dirname } from 'path'
6
6
  import { paths } from '../config/paths'
7
7
  import { logDebug, logWarning } from './error-handler'
8
+ import { platformService } from './platform-service'
8
9
  import type {
9
10
  SpinDBConfig,
10
11
  BinaryConfig,
@@ -165,73 +166,12 @@ export class ConfigManager {
165
166
 
166
167
  /**
167
168
  * Detect a binary on the system PATH
169
+ * Uses platformService for cross-platform detection (handles which/where and .exe extension)
168
170
  */
169
171
  async detectSystemBinary(tool: BinaryTool): Promise<string | null> {
170
- try {
171
- const { stdout } = await execAsync(`which ${tool}`)
172
- const path = stdout.trim()
173
- if (path && existsSync(path)) {
174
- return path
175
- }
176
- } catch (error) {
177
- logDebug('which command failed for binary detection', {
178
- tool,
179
- error: error instanceof Error ? error.message : String(error),
180
- })
181
- }
182
-
183
- // Check common locations
184
- const commonPaths = this.getCommonBinaryPaths(tool)
185
- for (const path of commonPaths) {
186
- if (existsSync(path)) {
187
- return path
188
- }
189
- }
190
-
191
- return null
192
- }
193
-
194
- /**
195
- * Get common installation paths for database tools
196
- */
197
- private getCommonBinaryPaths(tool: BinaryTool): string[] {
198
- const commonPaths: string[] = []
199
-
200
- // Homebrew (macOS ARM)
201
- commonPaths.push(`/opt/homebrew/bin/${tool}`)
202
- // Homebrew (macOS Intel)
203
- commonPaths.push(`/usr/local/bin/${tool}`)
204
-
205
- // PostgreSQL-specific paths
206
- if (POSTGRESQL_TOOLS.includes(tool) || tool === 'pgcli') {
207
- commonPaths.push(`/opt/homebrew/opt/libpq/bin/${tool}`)
208
- commonPaths.push(`/usr/local/opt/libpq/bin/${tool}`)
209
- // Postgres.app (macOS)
210
- commonPaths.push(
211
- `/Applications/Postgres.app/Contents/Versions/latest/bin/${tool}`,
212
- )
213
- // Linux PostgreSQL paths
214
- commonPaths.push(`/usr/lib/postgresql/17/bin/${tool}`)
215
- commonPaths.push(`/usr/lib/postgresql/16/bin/${tool}`)
216
- commonPaths.push(`/usr/lib/postgresql/15/bin/${tool}`)
217
- commonPaths.push(`/usr/lib/postgresql/14/bin/${tool}`)
218
- }
219
-
220
- // MySQL-specific paths
221
- if (MYSQL_TOOLS.includes(tool) || tool === 'mycli') {
222
- commonPaths.push(`/opt/homebrew/opt/mysql/bin/${tool}`)
223
- commonPaths.push(`/opt/homebrew/opt/mysql-client/bin/${tool}`)
224
- commonPaths.push(`/usr/local/opt/mysql/bin/${tool}`)
225
- commonPaths.push(`/usr/local/opt/mysql-client/bin/${tool}`)
226
- // Linux MySQL/MariaDB paths
227
- commonPaths.push(`/usr/bin/${tool}`)
228
- commonPaths.push(`/usr/sbin/${tool}`)
229
- }
230
-
231
- // General Linux paths
232
- commonPaths.push(`/usr/bin/${tool}`)
233
-
234
- return commonPaths
172
+ // Use platformService which handles cross-platform differences
173
+ // (which vs where, .exe extension, platform-specific search paths)
174
+ return platformService.findToolPath(tool)
235
175
  }
236
176
 
237
177
  /**
@@ -22,9 +22,37 @@ import {
22
22
  } from '../config/os-dependencies'
23
23
  import { platformService } from './platform-service'
24
24
  import { configManager } from './config-manager'
25
+ import type { BinaryTool } from '../types'
25
26
 
26
27
  const execAsync = promisify(exec)
27
28
 
29
+ /**
30
+ * Known binary tools that can be registered in config
31
+ */
32
+ const KNOWN_BINARY_TOOLS: readonly BinaryTool[] = [
33
+ 'psql',
34
+ 'pg_dump',
35
+ 'pg_restore',
36
+ 'pg_basebackup',
37
+ 'mysql',
38
+ 'mysqldump',
39
+ 'mysqlpump',
40
+ 'mysqld',
41
+ 'mysqladmin',
42
+ 'sqlite3',
43
+ 'pgcli',
44
+ 'mycli',
45
+ 'litecli',
46
+ 'usql',
47
+ ] as const
48
+
49
+ /**
50
+ * Type guard to check if a string is a known BinaryTool
51
+ */
52
+ function isBinaryTool(binary: string): binary is BinaryTool {
53
+ return KNOWN_BINARY_TOOLS.includes(binary as BinaryTool)
54
+ }
55
+
28
56
  export type DependencyStatus = {
29
57
  dependency: Dependency
30
58
  installed: boolean
@@ -79,7 +107,17 @@ export async function findBinary(
79
107
  binary: string,
80
108
  ): Promise<{ path: string; version?: string } | null> {
81
109
  try {
82
- // Use platformService to find the binary path
110
+ // First check if we have this binary registered in config (e.g., from downloaded PostgreSQL)
111
+ if (isBinaryTool(binary)) {
112
+ const configPath = await configManager.getBinaryPath(binary)
113
+ if (configPath) {
114
+ const version =
115
+ (await platformService.getToolVersion(configPath)) || undefined
116
+ return { path: configPath, version }
117
+ }
118
+ }
119
+
120
+ // Fall back to system PATH search
83
121
  const path = await platformService.findToolPath(binary)
84
122
  if (!path) return null
85
123