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.
- package/README.md +137 -8
- package/cli/commands/connect.ts +8 -4
- package/cli/commands/create.ts +106 -67
- package/cli/commands/deps.ts +19 -4
- package/cli/commands/edit.ts +245 -0
- package/cli/commands/engines.ts +434 -0
- package/cli/commands/info.ts +279 -0
- package/cli/commands/menu.ts +408 -153
- package/cli/commands/restore.ts +10 -24
- package/cli/commands/start.ts +25 -20
- package/cli/commands/url.ts +79 -0
- package/cli/index.ts +9 -3
- package/cli/ui/prompts.ts +8 -6
- package/config/engine-defaults.ts +24 -1
- package/config/os-dependencies.ts +59 -113
- package/config/paths.ts +7 -36
- package/core/binary-manager.ts +19 -6
- package/core/config-manager.ts +17 -5
- package/core/dependency-manager.ts +9 -15
- package/core/error-handler.ts +336 -0
- package/core/platform-service.ts +634 -0
- package/core/port-manager.ts +11 -3
- package/core/process-manager.ts +12 -2
- package/core/start-with-retry.ts +167 -0
- package/core/transaction-manager.ts +170 -0
- package/engines/mysql/binary-detection.ts +177 -100
- package/engines/mysql/index.ts +240 -131
- package/engines/mysql/restore.ts +257 -0
- package/engines/mysql/version-validator.ts +373 -0
- package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
- package/engines/postgresql/binary-urls.ts +5 -3
- package/engines/postgresql/index.ts +4 -3
- package/engines/postgresql/restore.ts +54 -5
- package/engines/postgresql/version-validator.ts +262 -0
- package/package.json +6 -2
- package/cli/commands/postgres-tools.ts +0 -216
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Service
|
|
3
|
+
*
|
|
4
|
+
* Centralizes all OS-specific detection and behavior, similar to how
|
|
5
|
+
* the engine abstraction handles database-specific behavior.
|
|
6
|
+
*
|
|
7
|
+
* This enables:
|
|
8
|
+
* - Consistent platform detection across the codebase
|
|
9
|
+
* - Easy mocking for unit tests
|
|
10
|
+
* - Simple addition of new platforms (e.g., Windows)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { homedir, platform as osPlatform, arch as osArch } from 'os'
|
|
14
|
+
import { execSync, exec, spawn } from 'child_process'
|
|
15
|
+
import { promisify } from 'util'
|
|
16
|
+
import { existsSync } from 'fs'
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec)
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export type Platform = 'darwin' | 'linux' | 'win32'
|
|
25
|
+
export type Architecture = 'arm64' | 'x64'
|
|
26
|
+
|
|
27
|
+
export type PlatformInfo = {
|
|
28
|
+
platform: Platform
|
|
29
|
+
arch: Architecture
|
|
30
|
+
homeDir: string
|
|
31
|
+
isWSL: boolean
|
|
32
|
+
isSudo: boolean
|
|
33
|
+
sudoUser: string | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ClipboardConfig = {
|
|
37
|
+
copyCommand: string
|
|
38
|
+
copyArgs: string[]
|
|
39
|
+
pasteCommand: string
|
|
40
|
+
pasteArgs: string[]
|
|
41
|
+
available: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type WhichCommandConfig = {
|
|
45
|
+
command: string
|
|
46
|
+
args: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PackageManagerInfo = {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
checkCommand: string
|
|
53
|
+
installTemplate: string
|
|
54
|
+
updateCommand: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Abstract Base Class
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
export abstract class BasePlatformService {
|
|
62
|
+
protected cachedPlatformInfo: PlatformInfo | null = null
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get platform information
|
|
66
|
+
*/
|
|
67
|
+
abstract getPlatformInfo(): PlatformInfo
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get clipboard configuration for this platform
|
|
71
|
+
*/
|
|
72
|
+
abstract getClipboardConfig(): ClipboardConfig
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the "which" command equivalent for this platform
|
|
76
|
+
*/
|
|
77
|
+
abstract getWhichCommand(): WhichCommandConfig
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get common search paths for a tool on this platform
|
|
81
|
+
*/
|
|
82
|
+
abstract getSearchPaths(tool: string): string[]
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect available package manager
|
|
86
|
+
*/
|
|
87
|
+
abstract detectPackageManager(): Promise<PackageManagerInfo | null>
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the zonky.io platform identifier for PostgreSQL binaries
|
|
91
|
+
*/
|
|
92
|
+
abstract getZonkyPlatform(): string | null
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Copy text to clipboard
|
|
96
|
+
*/
|
|
97
|
+
async copyToClipboard(text: string): Promise<boolean> {
|
|
98
|
+
const config = this.getClipboardConfig()
|
|
99
|
+
if (!config.available) return false
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await new Promise<void>((resolve, reject) => {
|
|
103
|
+
const proc = spawn(config.copyCommand, config.copyArgs, {
|
|
104
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
105
|
+
})
|
|
106
|
+
proc.stdin?.write(text)
|
|
107
|
+
proc.stdin?.end()
|
|
108
|
+
proc.on('close', (code) => {
|
|
109
|
+
if (code === 0) resolve()
|
|
110
|
+
else reject(new Error(`Clipboard command exited with code ${code}`))
|
|
111
|
+
})
|
|
112
|
+
proc.on('error', reject)
|
|
113
|
+
})
|
|
114
|
+
return true
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a tool is installed and return its path
|
|
122
|
+
*/
|
|
123
|
+
async findToolPath(toolName: string): Promise<string | null> {
|
|
124
|
+
const whichConfig = this.getWhichCommand()
|
|
125
|
+
|
|
126
|
+
// First try the which/where command
|
|
127
|
+
try {
|
|
128
|
+
const { stdout } = await execAsync(`${whichConfig.command} ${toolName}`)
|
|
129
|
+
const path = stdout.trim().split('\n')[0]
|
|
130
|
+
if (path && existsSync(path)) {
|
|
131
|
+
return path
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Not found via which, continue to search paths
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Search common installation paths
|
|
138
|
+
const searchPaths = this.getSearchPaths(toolName)
|
|
139
|
+
for (const dir of searchPaths) {
|
|
140
|
+
const fullPath = this.buildToolPath(dir, toolName)
|
|
141
|
+
if (existsSync(fullPath)) {
|
|
142
|
+
return fullPath
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build the full path to a tool in a directory
|
|
151
|
+
*/
|
|
152
|
+
protected abstract buildToolPath(dir: string, toolName: string): string
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get tool version by running --version
|
|
156
|
+
*/
|
|
157
|
+
async getToolVersion(toolPath: string): Promise<string | null> {
|
|
158
|
+
try {
|
|
159
|
+
const { stdout } = await execAsync(`"${toolPath}" --version`)
|
|
160
|
+
const match = stdout.match(/(\d+\.\d+(\.\d+)?)/)
|
|
161
|
+
return match ? match[1] : null
|
|
162
|
+
} catch {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Darwin (macOS) Implementation
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
class DarwinPlatformService extends BasePlatformService {
|
|
173
|
+
getPlatformInfo(): PlatformInfo {
|
|
174
|
+
if (this.cachedPlatformInfo) return this.cachedPlatformInfo
|
|
175
|
+
|
|
176
|
+
const sudoUser = process.env.SUDO_USER || null
|
|
177
|
+
let homeDir: string
|
|
178
|
+
|
|
179
|
+
if (sudoUser) {
|
|
180
|
+
// Running under sudo - get original user's home
|
|
181
|
+
try {
|
|
182
|
+
const result = execSync(`getent passwd ${sudoUser}`, {
|
|
183
|
+
encoding: 'utf-8',
|
|
184
|
+
})
|
|
185
|
+
const parts = result.trim().split(':')
|
|
186
|
+
homeDir =
|
|
187
|
+
parts.length >= 6 && parts[5] ? parts[5] : `/Users/${sudoUser}`
|
|
188
|
+
} catch {
|
|
189
|
+
homeDir = `/Users/${sudoUser}`
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
homeDir = homedir()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.cachedPlatformInfo = {
|
|
196
|
+
platform: 'darwin',
|
|
197
|
+
arch: osArch() as Architecture,
|
|
198
|
+
homeDir,
|
|
199
|
+
isWSL: false,
|
|
200
|
+
isSudo: !!sudoUser,
|
|
201
|
+
sudoUser,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.cachedPlatformInfo
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getClipboardConfig(): ClipboardConfig {
|
|
208
|
+
return {
|
|
209
|
+
copyCommand: 'pbcopy',
|
|
210
|
+
copyArgs: [],
|
|
211
|
+
pasteCommand: 'pbpaste',
|
|
212
|
+
pasteArgs: [],
|
|
213
|
+
available: true, // pbcopy is always available on macOS
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getWhichCommand(): WhichCommandConfig {
|
|
218
|
+
return {
|
|
219
|
+
command: 'which',
|
|
220
|
+
args: [],
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getSearchPaths(tool: string): string[] {
|
|
225
|
+
const paths: string[] = []
|
|
226
|
+
|
|
227
|
+
// MySQL-specific paths
|
|
228
|
+
if (
|
|
229
|
+
tool === 'mysqld' ||
|
|
230
|
+
tool === 'mysql' ||
|
|
231
|
+
tool === 'mysqladmin' ||
|
|
232
|
+
tool === 'mysqldump'
|
|
233
|
+
) {
|
|
234
|
+
paths.push(
|
|
235
|
+
// Homebrew (Apple Silicon)
|
|
236
|
+
'/opt/homebrew/bin',
|
|
237
|
+
'/opt/homebrew/opt/mysql/bin',
|
|
238
|
+
'/opt/homebrew/opt/mysql@8.0/bin',
|
|
239
|
+
'/opt/homebrew/opt/mysql@8.4/bin',
|
|
240
|
+
'/opt/homebrew/opt/mysql@5.7/bin',
|
|
241
|
+
// Homebrew (Intel)
|
|
242
|
+
'/usr/local/bin',
|
|
243
|
+
'/usr/local/opt/mysql/bin',
|
|
244
|
+
'/usr/local/opt/mysql@8.0/bin',
|
|
245
|
+
'/usr/local/opt/mysql@8.4/bin',
|
|
246
|
+
'/usr/local/opt/mysql@5.7/bin',
|
|
247
|
+
// Official MySQL installer
|
|
248
|
+
'/usr/local/mysql/bin',
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// PostgreSQL-specific paths
|
|
253
|
+
if (
|
|
254
|
+
tool === 'psql' ||
|
|
255
|
+
tool === 'pg_dump' ||
|
|
256
|
+
tool === 'pg_restore' ||
|
|
257
|
+
tool === 'pg_basebackup'
|
|
258
|
+
) {
|
|
259
|
+
paths.push(
|
|
260
|
+
// Homebrew (Apple Silicon)
|
|
261
|
+
'/opt/homebrew/bin',
|
|
262
|
+
'/opt/homebrew/opt/postgresql/bin',
|
|
263
|
+
'/opt/homebrew/opt/postgresql@17/bin',
|
|
264
|
+
'/opt/homebrew/opt/postgresql@16/bin',
|
|
265
|
+
'/opt/homebrew/opt/postgresql@15/bin',
|
|
266
|
+
'/opt/homebrew/opt/postgresql@14/bin',
|
|
267
|
+
// Homebrew (Intel)
|
|
268
|
+
'/usr/local/bin',
|
|
269
|
+
'/usr/local/opt/postgresql/bin',
|
|
270
|
+
'/usr/local/opt/postgresql@17/bin',
|
|
271
|
+
'/usr/local/opt/postgresql@16/bin',
|
|
272
|
+
'/usr/local/opt/postgresql@15/bin',
|
|
273
|
+
'/usr/local/opt/postgresql@14/bin',
|
|
274
|
+
// Postgres.app
|
|
275
|
+
'/Applications/Postgres.app/Contents/Versions/latest/bin',
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Generic paths
|
|
280
|
+
paths.push('/usr/local/bin', '/usr/bin')
|
|
281
|
+
|
|
282
|
+
return paths
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async detectPackageManager(): Promise<PackageManagerInfo | null> {
|
|
286
|
+
try {
|
|
287
|
+
await execAsync('brew --version')
|
|
288
|
+
return {
|
|
289
|
+
id: 'brew',
|
|
290
|
+
name: 'Homebrew',
|
|
291
|
+
checkCommand: 'brew --version',
|
|
292
|
+
installTemplate: 'brew install {package}',
|
|
293
|
+
updateCommand: 'brew update',
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
getZonkyPlatform(): string | null {
|
|
301
|
+
const arch = osArch()
|
|
302
|
+
if (arch === 'arm64') return 'darwin-arm64v8'
|
|
303
|
+
if (arch === 'x64') return 'darwin-amd64'
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
protected buildToolPath(dir: string, toolName: string): string {
|
|
308
|
+
return `${dir}/${toolName}`
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// =============================================================================
|
|
313
|
+
// Linux Implementation
|
|
314
|
+
// =============================================================================
|
|
315
|
+
|
|
316
|
+
class LinuxPlatformService extends BasePlatformService {
|
|
317
|
+
getPlatformInfo(): PlatformInfo {
|
|
318
|
+
if (this.cachedPlatformInfo) return this.cachedPlatformInfo
|
|
319
|
+
|
|
320
|
+
const sudoUser = process.env.SUDO_USER || null
|
|
321
|
+
let homeDir: string
|
|
322
|
+
|
|
323
|
+
if (sudoUser) {
|
|
324
|
+
try {
|
|
325
|
+
const result = execSync(`getent passwd ${sudoUser}`, {
|
|
326
|
+
encoding: 'utf-8',
|
|
327
|
+
})
|
|
328
|
+
const parts = result.trim().split(':')
|
|
329
|
+
homeDir = parts.length >= 6 && parts[5] ? parts[5] : `/home/${sudoUser}`
|
|
330
|
+
} catch {
|
|
331
|
+
homeDir = `/home/${sudoUser}`
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
homeDir = homedir()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check if running in WSL
|
|
338
|
+
let isWSL = false
|
|
339
|
+
try {
|
|
340
|
+
const uname = execSync('uname -r', { encoding: 'utf-8' })
|
|
341
|
+
isWSL = uname.toLowerCase().includes('microsoft')
|
|
342
|
+
} catch {
|
|
343
|
+
// Not WSL
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.cachedPlatformInfo = {
|
|
347
|
+
platform: 'linux',
|
|
348
|
+
arch: osArch() as Architecture,
|
|
349
|
+
homeDir,
|
|
350
|
+
isWSL,
|
|
351
|
+
isSudo: !!sudoUser,
|
|
352
|
+
sudoUser,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return this.cachedPlatformInfo
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getClipboardConfig(): ClipboardConfig {
|
|
359
|
+
// Check if xclip is available
|
|
360
|
+
let available = false
|
|
361
|
+
try {
|
|
362
|
+
execSync('which xclip', { encoding: 'utf-8' })
|
|
363
|
+
available = true
|
|
364
|
+
} catch {
|
|
365
|
+
// xclip not installed
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
copyCommand: 'xclip',
|
|
370
|
+
copyArgs: ['-selection', 'clipboard'],
|
|
371
|
+
pasteCommand: 'xclip',
|
|
372
|
+
pasteArgs: ['-selection', 'clipboard', '-o'],
|
|
373
|
+
available,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getWhichCommand(): WhichCommandConfig {
|
|
378
|
+
return {
|
|
379
|
+
command: 'which',
|
|
380
|
+
args: [],
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
getSearchPaths(tool: string): string[] {
|
|
385
|
+
const paths: string[] = []
|
|
386
|
+
|
|
387
|
+
// MySQL-specific paths
|
|
388
|
+
if (
|
|
389
|
+
tool === 'mysqld' ||
|
|
390
|
+
tool === 'mysql' ||
|
|
391
|
+
tool === 'mysqladmin' ||
|
|
392
|
+
tool === 'mysqldump'
|
|
393
|
+
) {
|
|
394
|
+
paths.push(
|
|
395
|
+
'/usr/bin',
|
|
396
|
+
'/usr/sbin',
|
|
397
|
+
'/usr/local/bin',
|
|
398
|
+
'/usr/local/mysql/bin',
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// PostgreSQL-specific paths
|
|
403
|
+
if (
|
|
404
|
+
tool === 'psql' ||
|
|
405
|
+
tool === 'pg_dump' ||
|
|
406
|
+
tool === 'pg_restore' ||
|
|
407
|
+
tool === 'pg_basebackup'
|
|
408
|
+
) {
|
|
409
|
+
paths.push(
|
|
410
|
+
'/usr/bin',
|
|
411
|
+
'/usr/local/bin',
|
|
412
|
+
'/usr/lib/postgresql/17/bin',
|
|
413
|
+
'/usr/lib/postgresql/16/bin',
|
|
414
|
+
'/usr/lib/postgresql/15/bin',
|
|
415
|
+
'/usr/lib/postgresql/14/bin',
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Generic paths
|
|
420
|
+
paths.push('/usr/bin', '/usr/local/bin')
|
|
421
|
+
|
|
422
|
+
return paths
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async detectPackageManager(): Promise<PackageManagerInfo | null> {
|
|
426
|
+
// Try apt first (Debian/Ubuntu)
|
|
427
|
+
try {
|
|
428
|
+
await execAsync('apt --version')
|
|
429
|
+
return {
|
|
430
|
+
id: 'apt',
|
|
431
|
+
name: 'APT',
|
|
432
|
+
checkCommand: 'apt --version',
|
|
433
|
+
installTemplate: 'sudo apt install -y {package}',
|
|
434
|
+
updateCommand: 'sudo apt update',
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
// Not apt
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Try dnf (Fedora/RHEL 8+)
|
|
441
|
+
try {
|
|
442
|
+
await execAsync('dnf --version')
|
|
443
|
+
return {
|
|
444
|
+
id: 'dnf',
|
|
445
|
+
name: 'DNF',
|
|
446
|
+
checkCommand: 'dnf --version',
|
|
447
|
+
installTemplate: 'sudo dnf install -y {package}',
|
|
448
|
+
updateCommand: 'sudo dnf check-update',
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
// Not dnf
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Try yum (RHEL/CentOS 7)
|
|
455
|
+
try {
|
|
456
|
+
await execAsync('yum --version')
|
|
457
|
+
return {
|
|
458
|
+
id: 'yum',
|
|
459
|
+
name: 'YUM',
|
|
460
|
+
checkCommand: 'yum --version',
|
|
461
|
+
installTemplate: 'sudo yum install -y {package}',
|
|
462
|
+
updateCommand: 'sudo yum check-update',
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
// Not yum
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Try pacman (Arch)
|
|
469
|
+
try {
|
|
470
|
+
await execAsync('pacman --version')
|
|
471
|
+
return {
|
|
472
|
+
id: 'pacman',
|
|
473
|
+
name: 'Pacman',
|
|
474
|
+
checkCommand: 'pacman --version',
|
|
475
|
+
installTemplate: 'sudo pacman -S --noconfirm {package}',
|
|
476
|
+
updateCommand: 'sudo pacman -Sy',
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
// Not pacman
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
getZonkyPlatform(): string | null {
|
|
486
|
+
const arch = osArch()
|
|
487
|
+
if (arch === 'arm64') return 'linux-arm64v8'
|
|
488
|
+
if (arch === 'x64') return 'linux-amd64'
|
|
489
|
+
return null
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
protected buildToolPath(dir: string, toolName: string): string {
|
|
493
|
+
return `${dir}/${toolName}`
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// Windows Implementation (Stub for future support)
|
|
499
|
+
// =============================================================================
|
|
500
|
+
|
|
501
|
+
class Win32PlatformService extends BasePlatformService {
|
|
502
|
+
getPlatformInfo(): PlatformInfo {
|
|
503
|
+
if (this.cachedPlatformInfo) return this.cachedPlatformInfo
|
|
504
|
+
|
|
505
|
+
this.cachedPlatformInfo = {
|
|
506
|
+
platform: 'win32',
|
|
507
|
+
arch: osArch() as Architecture,
|
|
508
|
+
homeDir: homedir(),
|
|
509
|
+
isWSL: false,
|
|
510
|
+
isSudo: false,
|
|
511
|
+
sudoUser: null,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return this.cachedPlatformInfo
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
getClipboardConfig(): ClipboardConfig {
|
|
518
|
+
return {
|
|
519
|
+
copyCommand: 'clip',
|
|
520
|
+
copyArgs: [],
|
|
521
|
+
pasteCommand: 'powershell',
|
|
522
|
+
pasteArgs: ['-command', 'Get-Clipboard'],
|
|
523
|
+
available: true,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
getWhichCommand(): WhichCommandConfig {
|
|
528
|
+
return {
|
|
529
|
+
command: 'where',
|
|
530
|
+
args: [],
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
getSearchPaths(tool: string): string[] {
|
|
535
|
+
const paths: string[] = []
|
|
536
|
+
|
|
537
|
+
// MySQL-specific paths
|
|
538
|
+
if (
|
|
539
|
+
tool === 'mysqld' ||
|
|
540
|
+
tool === 'mysql' ||
|
|
541
|
+
tool === 'mysqladmin' ||
|
|
542
|
+
tool === 'mysqldump'
|
|
543
|
+
) {
|
|
544
|
+
paths.push(
|
|
545
|
+
'C:\\Program Files\\MySQL\\MySQL Server 8.0\\bin',
|
|
546
|
+
'C:\\Program Files\\MySQL\\MySQL Server 8.4\\bin',
|
|
547
|
+
'C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin',
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// PostgreSQL-specific paths
|
|
552
|
+
if (
|
|
553
|
+
tool === 'psql' ||
|
|
554
|
+
tool === 'pg_dump' ||
|
|
555
|
+
tool === 'pg_restore' ||
|
|
556
|
+
tool === 'pg_basebackup'
|
|
557
|
+
) {
|
|
558
|
+
paths.push(
|
|
559
|
+
'C:\\Program Files\\PostgreSQL\\17\\bin',
|
|
560
|
+
'C:\\Program Files\\PostgreSQL\\16\\bin',
|
|
561
|
+
'C:\\Program Files\\PostgreSQL\\15\\bin',
|
|
562
|
+
'C:\\Program Files\\PostgreSQL\\14\\bin',
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return paths
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async detectPackageManager(): Promise<PackageManagerInfo | null> {
|
|
570
|
+
// Try chocolatey
|
|
571
|
+
try {
|
|
572
|
+
await execAsync('choco --version')
|
|
573
|
+
return {
|
|
574
|
+
id: 'choco',
|
|
575
|
+
name: 'Chocolatey',
|
|
576
|
+
checkCommand: 'choco --version',
|
|
577
|
+
installTemplate: 'choco install -y {package}',
|
|
578
|
+
updateCommand: 'choco upgrade all',
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
// Not chocolatey
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Try winget
|
|
585
|
+
try {
|
|
586
|
+
await execAsync('winget --version')
|
|
587
|
+
return {
|
|
588
|
+
id: 'winget',
|
|
589
|
+
name: 'Windows Package Manager',
|
|
590
|
+
checkCommand: 'winget --version',
|
|
591
|
+
installTemplate: 'winget install {package}',
|
|
592
|
+
updateCommand: 'winget upgrade --all',
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
// Not winget
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return null
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
getZonkyPlatform(): string | null {
|
|
602
|
+
// zonky.io doesn't provide Windows binaries
|
|
603
|
+
return null
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
protected buildToolPath(dir: string, toolName: string): string {
|
|
607
|
+
return `${dir}\\${toolName}.exe`
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// =============================================================================
|
|
612
|
+
// Factory and Singleton
|
|
613
|
+
// =============================================================================
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Create the appropriate platform service for the current OS
|
|
617
|
+
*/
|
|
618
|
+
export function createPlatformService(): BasePlatformService {
|
|
619
|
+
const platform = osPlatform()
|
|
620
|
+
|
|
621
|
+
switch (platform) {
|
|
622
|
+
case 'darwin':
|
|
623
|
+
return new DarwinPlatformService()
|
|
624
|
+
case 'linux':
|
|
625
|
+
return new LinuxPlatformService()
|
|
626
|
+
case 'win32':
|
|
627
|
+
return new Win32PlatformService()
|
|
628
|
+
default:
|
|
629
|
+
throw new Error(`Unsupported platform: ${platform}`)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Export singleton instance for convenience
|
|
634
|
+
export const platformService = createPlatformService()
|
package/core/port-manager.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync } from 'fs'
|
|
|
5
5
|
import { readdir, readFile } from 'fs/promises'
|
|
6
6
|
import { defaults, getSupportedEngines } from '../config/defaults'
|
|
7
7
|
import { paths } from '../config/paths'
|
|
8
|
+
import { logDebug } from './error-handler'
|
|
8
9
|
import type { ContainerConfig, PortResult } from '../types'
|
|
9
10
|
|
|
10
11
|
const execAsync = promisify(exec)
|
|
@@ -83,7 +84,11 @@ export class PortManager {
|
|
|
83
84
|
try {
|
|
84
85
|
const { stdout } = await execAsync(`lsof -i :${port} -P -n | head -5`)
|
|
85
86
|
return stdout.trim()
|
|
86
|
-
} catch {
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logDebug('Could not determine port user', {
|
|
89
|
+
port,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
})
|
|
87
92
|
return null
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -116,8 +121,11 @@ export class PortManager {
|
|
|
116
121
|
const content = await readFile(configPath, 'utf8')
|
|
117
122
|
const config = JSON.parse(content) as ContainerConfig
|
|
118
123
|
ports.push(config.port)
|
|
119
|
-
} catch {
|
|
120
|
-
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logDebug('Skipping invalid container config', {
|
|
126
|
+
configPath,
|
|
127
|
+
error: error instanceof Error ? error.message : String(error),
|
|
128
|
+
})
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
}
|
package/core/process-manager.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { promisify } from 'util'
|
|
|
3
3
|
import { existsSync } from 'fs'
|
|
4
4
|
import { readFile } from 'fs/promises'
|
|
5
5
|
import { paths } from '../config/paths'
|
|
6
|
+
import { logDebug } from './error-handler'
|
|
6
7
|
import type { ProcessResult, StatusResult } from '../types'
|
|
7
8
|
|
|
8
9
|
const execAsync = promisify(exec)
|
|
@@ -221,7 +222,12 @@ export class ProcessManager {
|
|
|
221
222
|
// Check if process is still running
|
|
222
223
|
process.kill(pid, 0)
|
|
223
224
|
return true
|
|
224
|
-
} catch {
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logDebug('PID file check failed', {
|
|
227
|
+
containerName,
|
|
228
|
+
engine: options.engine,
|
|
229
|
+
error: error instanceof Error ? error.message : String(error),
|
|
230
|
+
})
|
|
225
231
|
return false
|
|
226
232
|
}
|
|
227
233
|
}
|
|
@@ -242,7 +248,11 @@ export class ProcessManager {
|
|
|
242
248
|
try {
|
|
243
249
|
const content = await readFile(pidFile, 'utf8')
|
|
244
250
|
return parseInt(content.split('\n')[0], 10)
|
|
245
|
-
} catch {
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logDebug('Failed to read PID file', {
|
|
253
|
+
pidFile,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error),
|
|
255
|
+
})
|
|
246
256
|
return null
|
|
247
257
|
}
|
|
248
258
|
}
|