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.
- package/README.md +19 -10
- package/cli/commands/create.ts +72 -42
- package/cli/commands/engines.ts +61 -0
- package/cli/commands/logs.ts +3 -29
- package/cli/commands/menu/container-handlers.ts +32 -3
- package/cli/commands/menu/sql-handlers.ts +4 -26
- package/cli/helpers.ts +6 -6
- package/cli/index.ts +3 -3
- package/cli/utils/file-follower.ts +95 -0
- package/config/defaults.ts +3 -0
- package/config/os-dependencies.ts +79 -1
- package/core/binary-manager.ts +181 -66
- package/core/config-manager.ts +5 -65
- package/core/dependency-manager.ts +39 -1
- package/core/platform-service.ts +149 -11
- package/core/process-manager.ts +152 -33
- package/engines/base-engine.ts +27 -0
- package/engines/mysql/backup.ts +12 -5
- package/engines/mysql/index.ts +328 -110
- package/engines/mysql/restore.ts +22 -6
- package/engines/postgresql/backup.ts +7 -3
- package/engines/postgresql/binary-manager.ts +47 -31
- package/engines/postgresql/edb-binary-urls.ts +123 -0
- package/engines/postgresql/index.ts +109 -22
- package/engines/postgresql/version-maps.ts +63 -0
- package/engines/sqlite/index.ts +9 -19
- package/package.json +4 -2
|
@@ -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
|
],
|
package/core/binary-manager.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
114
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
158
|
+
const fileStream = createWriteStream(archiveFile)
|
|
153
159
|
// @ts-expect-error - response.body is ReadableStream
|
|
154
160
|
await pipeline(response.body, fileStream)
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
//
|
|
233
|
-
const
|
|
234
|
-
const expectedNormalized =
|
|
235
|
-
const reportedNormalized =
|
|
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
|
-
|
|
388
|
+
const ext = platform === 'win32' ? '.exe' : ''
|
|
389
|
+
return join(binPath, 'bin', `${binary}${ext}`)
|
|
275
390
|
}
|
|
276
391
|
|
|
277
392
|
/**
|
package/core/config-manager.ts
CHANGED
|
@@ -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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
//
|
|
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
|
|