spindb 0.6.0 → 0.7.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 +421 -294
- package/cli/commands/config.ts +7 -1
- package/cli/commands/connect.ts +1 -0
- package/cli/commands/create.ts +7 -7
- package/cli/commands/edit.ts +10 -0
- package/cli/commands/engines.ts +10 -188
- package/cli/commands/info.ts +7 -14
- package/cli/commands/list.ts +2 -9
- package/cli/commands/logs.ts +130 -0
- package/cli/commands/menu/backup-handlers.ts +798 -0
- package/cli/commands/menu/container-handlers.ts +832 -0
- package/cli/commands/menu/engine-handlers.ts +382 -0
- package/cli/commands/menu/index.ts +184 -0
- package/cli/commands/menu/shared.ts +26 -0
- package/cli/commands/menu/shell-handlers.ts +331 -0
- package/cli/commands/menu/sql-handlers.ts +197 -0
- package/cli/commands/menu/update-handlers.ts +94 -0
- package/cli/commands/run.ts +150 -0
- package/cli/commands/url.ts +19 -5
- package/cli/constants.ts +10 -0
- package/cli/helpers.ts +152 -0
- package/cli/index.ts +5 -2
- package/cli/ui/prompts.ts +3 -11
- package/config/defaults.ts +5 -29
- package/core/binary-manager.ts +2 -2
- package/core/container-manager.ts +3 -2
- package/core/dependency-manager.ts +0 -163
- package/core/error-handler.ts +0 -26
- package/core/platform-service.ts +60 -40
- package/core/start-with-retry.ts +3 -28
- package/core/transaction-manager.ts +0 -8
- package/engines/base-engine.ts +10 -0
- package/engines/mysql/binary-detection.ts +1 -1
- package/engines/mysql/index.ts +78 -2
- package/engines/postgresql/index.ts +49 -0
- package/package.json +1 -1
- package/types/index.ts +7 -4
- package/cli/commands/menu.ts +0 -2670
|
@@ -43,13 +43,6 @@ export type InstallResult = {
|
|
|
43
43
|
error?: string
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// =============================================================================
|
|
47
|
-
// Package Manager Detection
|
|
48
|
-
// =============================================================================
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Detect which package manager is available on the current system
|
|
52
|
-
*/
|
|
53
46
|
export async function detectPackageManager(): Promise<DetectedPackageManager | null> {
|
|
54
47
|
const { platform } = platformService.getPlatformInfo()
|
|
55
48
|
|
|
@@ -81,13 +74,6 @@ export function getCurrentPlatform(): Platform {
|
|
|
81
74
|
return platformService.getPlatformInfo().platform as Platform
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
// =============================================================================
|
|
85
|
-
// Dependency Checking
|
|
86
|
-
// =============================================================================
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Check if a binary is installed and get its path
|
|
90
|
-
*/
|
|
91
77
|
export async function findBinary(
|
|
92
78
|
binary: string,
|
|
93
79
|
): Promise<{ path: string; version?: string } | null> {
|
|
@@ -164,13 +150,6 @@ export async function getAllMissingDependencies(): Promise<Dependency[]> {
|
|
|
164
150
|
return statuses.filter((s) => !s.installed).map((s) => s.dependency)
|
|
165
151
|
}
|
|
166
152
|
|
|
167
|
-
// =============================================================================
|
|
168
|
-
// Installation
|
|
169
|
-
// =============================================================================
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Check if stdin is a TTY (interactive terminal)
|
|
173
|
-
*/
|
|
174
153
|
function hasTTY(): boolean {
|
|
175
154
|
return process.stdin.isTTY === true
|
|
176
155
|
}
|
|
@@ -362,13 +341,6 @@ export async function installAllDependencies(
|
|
|
362
341
|
return results
|
|
363
342
|
}
|
|
364
343
|
|
|
365
|
-
// =============================================================================
|
|
366
|
-
// Manual Installation Instructions
|
|
367
|
-
// =============================================================================
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Get manual installation instructions for a dependency
|
|
371
|
-
*/
|
|
372
344
|
export function getManualInstallInstructions(
|
|
373
345
|
dependency: Dependency,
|
|
374
346
|
platform: Platform = getCurrentPlatform(),
|
|
@@ -376,188 +348,53 @@ export function getManualInstallInstructions(
|
|
|
376
348
|
return dependency.manualInstall[platform] || []
|
|
377
349
|
}
|
|
378
350
|
|
|
379
|
-
/**
|
|
380
|
-
* Get manual installation instructions for all missing dependencies of an engine
|
|
381
|
-
*/
|
|
382
|
-
export function getEngineManualInstallInstructions(
|
|
383
|
-
engine: string,
|
|
384
|
-
missingDeps: Dependency[],
|
|
385
|
-
platform: Platform = getCurrentPlatform(),
|
|
386
|
-
): string[] {
|
|
387
|
-
// Since all deps usually come from the same package, just get instructions from the first one
|
|
388
|
-
if (missingDeps.length === 0) return []
|
|
389
|
-
|
|
390
|
-
return getManualInstallInstructions(missingDeps[0], platform)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// =============================================================================
|
|
394
|
-
// High-Level API
|
|
395
|
-
// =============================================================================
|
|
396
|
-
|
|
397
|
-
export type DependencyCheckResult = {
|
|
398
|
-
engine: string
|
|
399
|
-
allInstalled: boolean
|
|
400
|
-
installed: DependencyStatus[]
|
|
401
|
-
missing: DependencyStatus[]
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Get a complete dependency report for an engine
|
|
406
|
-
*/
|
|
407
|
-
export async function getDependencyReport(
|
|
408
|
-
engine: string,
|
|
409
|
-
): Promise<DependencyCheckResult> {
|
|
410
|
-
const statuses = await checkEngineDependencies(engine)
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
engine,
|
|
414
|
-
allInstalled: statuses.every((s) => s.installed),
|
|
415
|
-
installed: statuses.filter((s) => s.installed),
|
|
416
|
-
missing: statuses.filter((s) => !s.installed),
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Get dependency reports for all engines
|
|
422
|
-
*/
|
|
423
|
-
export async function getAllDependencyReports(): Promise<
|
|
424
|
-
DependencyCheckResult[]
|
|
425
|
-
> {
|
|
426
|
-
const engines = ['postgresql', 'mysql']
|
|
427
|
-
const reports = await Promise.all(
|
|
428
|
-
engines.map((engine) => getDependencyReport(engine)),
|
|
429
|
-
)
|
|
430
|
-
return reports
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// =============================================================================
|
|
434
|
-
// usql (Enhanced Shell) Support
|
|
435
|
-
// =============================================================================
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Check if usql is installed
|
|
439
|
-
*/
|
|
440
351
|
export async function isUsqlInstalled(): Promise<boolean> {
|
|
441
352
|
const status = await checkDependency(usqlDependency)
|
|
442
353
|
return status.installed
|
|
443
354
|
}
|
|
444
355
|
|
|
445
|
-
/**
|
|
446
|
-
* Get usql dependency status
|
|
447
|
-
*/
|
|
448
|
-
export async function getUsqlStatus(): Promise<DependencyStatus> {
|
|
449
|
-
return checkDependency(usqlDependency)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Install usql using the detected package manager
|
|
454
|
-
*/
|
|
455
356
|
export async function installUsql(
|
|
456
357
|
packageManager: DetectedPackageManager,
|
|
457
358
|
): Promise<InstallResult> {
|
|
458
359
|
return installDependency(usqlDependency, packageManager)
|
|
459
360
|
}
|
|
460
361
|
|
|
461
|
-
/**
|
|
462
|
-
* Get usql manual installation instructions
|
|
463
|
-
*/
|
|
464
362
|
export function getUsqlManualInstructions(
|
|
465
363
|
platform: Platform = getCurrentPlatform(),
|
|
466
364
|
): string[] {
|
|
467
365
|
return getManualInstallInstructions(usqlDependency, platform)
|
|
468
366
|
}
|
|
469
367
|
|
|
470
|
-
/**
|
|
471
|
-
* Get the usql dependency definition
|
|
472
|
-
*/
|
|
473
|
-
export function getUsqlDependency(): Dependency {
|
|
474
|
-
return usqlDependency
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// =============================================================================
|
|
478
|
-
// pgcli (PostgreSQL Enhanced Shell) Support
|
|
479
|
-
// =============================================================================
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Check if pgcli is installed
|
|
483
|
-
*/
|
|
484
368
|
export async function isPgcliInstalled(): Promise<boolean> {
|
|
485
369
|
const status = await checkDependency(pgcliDependency)
|
|
486
370
|
return status.installed
|
|
487
371
|
}
|
|
488
372
|
|
|
489
|
-
/**
|
|
490
|
-
* Get pgcli dependency status
|
|
491
|
-
*/
|
|
492
|
-
export async function getPgcliStatus(): Promise<DependencyStatus> {
|
|
493
|
-
return checkDependency(pgcliDependency)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Install pgcli using the detected package manager
|
|
498
|
-
*/
|
|
499
373
|
export async function installPgcli(
|
|
500
374
|
packageManager: DetectedPackageManager,
|
|
501
375
|
): Promise<InstallResult> {
|
|
502
376
|
return installDependency(pgcliDependency, packageManager)
|
|
503
377
|
}
|
|
504
378
|
|
|
505
|
-
/**
|
|
506
|
-
* Get pgcli manual installation instructions
|
|
507
|
-
*/
|
|
508
379
|
export function getPgcliManualInstructions(
|
|
509
380
|
platform: Platform = getCurrentPlatform(),
|
|
510
381
|
): string[] {
|
|
511
382
|
return getManualInstallInstructions(pgcliDependency, platform)
|
|
512
383
|
}
|
|
513
384
|
|
|
514
|
-
/**
|
|
515
|
-
* Get the pgcli dependency definition
|
|
516
|
-
*/
|
|
517
|
-
export function getPgcliDependency(): Dependency {
|
|
518
|
-
return pgcliDependency
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// =============================================================================
|
|
522
|
-
// mycli (MySQL Enhanced Shell) Support
|
|
523
|
-
// =============================================================================
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Check if mycli is installed
|
|
527
|
-
*/
|
|
528
385
|
export async function isMycliInstalled(): Promise<boolean> {
|
|
529
386
|
const status = await checkDependency(mycliDependency)
|
|
530
387
|
return status.installed
|
|
531
388
|
}
|
|
532
389
|
|
|
533
|
-
/**
|
|
534
|
-
* Get mycli dependency status
|
|
535
|
-
*/
|
|
536
|
-
export async function getMycliStatus(): Promise<DependencyStatus> {
|
|
537
|
-
return checkDependency(mycliDependency)
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Install mycli using the detected package manager
|
|
542
|
-
*/
|
|
543
390
|
export async function installMycli(
|
|
544
391
|
packageManager: DetectedPackageManager,
|
|
545
392
|
): Promise<InstallResult> {
|
|
546
393
|
return installDependency(mycliDependency, packageManager)
|
|
547
394
|
}
|
|
548
395
|
|
|
549
|
-
/**
|
|
550
|
-
* Get mycli manual installation instructions
|
|
551
|
-
*/
|
|
552
396
|
export function getMycliManualInstructions(
|
|
553
397
|
platform: Platform = getCurrentPlatform(),
|
|
554
398
|
): string[] {
|
|
555
399
|
return getManualInstallInstructions(mycliDependency, platform)
|
|
556
400
|
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Get the mycli dependency definition
|
|
560
|
-
*/
|
|
561
|
-
export function getMycliDependency(): Dependency {
|
|
562
|
-
return mycliDependency
|
|
563
|
-
}
|
package/core/error-handler.ts
CHANGED
|
@@ -17,10 +17,6 @@ function getSpinDBRoot(): string {
|
|
|
17
17
|
return join(home, '.spindb')
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// Types
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
20
|
export type ErrorSeverity = 'fatal' | 'error' | 'warning' | 'info'
|
|
25
21
|
|
|
26
22
|
export type SpinDBErrorInfo = {
|
|
@@ -31,10 +27,6 @@ export type SpinDBErrorInfo = {
|
|
|
31
27
|
context?: Record<string, unknown>
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
// =============================================================================
|
|
35
|
-
// Error Codes
|
|
36
|
-
// =============================================================================
|
|
37
|
-
|
|
38
30
|
export const ErrorCodes = {
|
|
39
31
|
// Port errors
|
|
40
32
|
PORT_IN_USE: 'PORT_IN_USE',
|
|
@@ -83,10 +75,6 @@ export const ErrorCodes = {
|
|
|
83
75
|
|
|
84
76
|
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
|
85
77
|
|
|
86
|
-
// =============================================================================
|
|
87
|
-
// SpinDBError Class
|
|
88
|
-
// =============================================================================
|
|
89
|
-
|
|
90
78
|
export class SpinDBError extends Error {
|
|
91
79
|
public readonly code: string
|
|
92
80
|
public readonly severity: ErrorSeverity
|
|
@@ -131,13 +119,6 @@ export class SpinDBError extends Error {
|
|
|
131
119
|
}
|
|
132
120
|
}
|
|
133
121
|
|
|
134
|
-
// =============================================================================
|
|
135
|
-
// Logging Functions
|
|
136
|
-
// =============================================================================
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get the path to the log file
|
|
140
|
-
*/
|
|
141
122
|
function getLogPath(): string {
|
|
142
123
|
return join(getSpinDBRoot(), 'spindb.log')
|
|
143
124
|
}
|
|
@@ -264,13 +245,6 @@ export function logDebug(
|
|
|
264
245
|
})
|
|
265
246
|
}
|
|
266
247
|
|
|
267
|
-
// =============================================================================
|
|
268
|
-
// Error Creation Helpers
|
|
269
|
-
// =============================================================================
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Create a port-in-use error with helpful suggestion
|
|
273
|
-
*/
|
|
274
248
|
export function createPortInUseError(port: number): SpinDBError {
|
|
275
249
|
return new SpinDBError(
|
|
276
250
|
ErrorCodes.PORT_IN_USE,
|
package/core/platform-service.ts
CHANGED
|
@@ -17,13 +17,47 @@ import { existsSync } from 'fs'
|
|
|
17
17
|
|
|
18
18
|
const execAsync = promisify(exec)
|
|
19
19
|
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// Types
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
20
|
export type Platform = 'darwin' | 'linux' | 'win32'
|
|
25
21
|
export type Architecture = 'arm64' | 'x64'
|
|
26
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Options for resolving home directory under sudo
|
|
25
|
+
*/
|
|
26
|
+
export type ResolveHomeDirOptions = {
|
|
27
|
+
sudoUser: string | null
|
|
28
|
+
getentResult: string | null
|
|
29
|
+
platform: 'darwin' | 'linux'
|
|
30
|
+
defaultHome: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the correct home directory, handling sudo scenarios.
|
|
35
|
+
* This is extracted as a pure function for testability.
|
|
36
|
+
*
|
|
37
|
+
* When running under sudo, we need to use the original user's home directory,
|
|
38
|
+
* not root's home. This prevents ~/.spindb from being created in /root/.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveHomeDir(options: ResolveHomeDirOptions): string {
|
|
41
|
+
const { sudoUser, getentResult, platform, defaultHome } = options
|
|
42
|
+
|
|
43
|
+
// Not running under sudo - use default
|
|
44
|
+
if (!sudoUser) {
|
|
45
|
+
return defaultHome
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try to parse home from getent passwd output
|
|
49
|
+
// Format: username:password:uid:gid:gecos:home:shell
|
|
50
|
+
if (getentResult) {
|
|
51
|
+
const parts = getentResult.trim().split(':')
|
|
52
|
+
if (parts.length >= 6 && parts[5]) {
|
|
53
|
+
return parts[5]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fallback to platform-specific default
|
|
58
|
+
return platform === 'darwin' ? `/Users/${sudoUser}` : `/home/${sudoUser}`
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
export type PlatformInfo = {
|
|
28
62
|
platform: Platform
|
|
29
63
|
arch: Architecture
|
|
@@ -54,10 +88,6 @@ export type PackageManagerInfo = {
|
|
|
54
88
|
updateCommand: string
|
|
55
89
|
}
|
|
56
90
|
|
|
57
|
-
// =============================================================================
|
|
58
|
-
// Abstract Base Class
|
|
59
|
-
// =============================================================================
|
|
60
|
-
|
|
61
91
|
export abstract class BasePlatformService {
|
|
62
92
|
protected cachedPlatformInfo: PlatformInfo | null = null
|
|
63
93
|
|
|
@@ -165,33 +195,31 @@ export abstract class BasePlatformService {
|
|
|
165
195
|
}
|
|
166
196
|
}
|
|
167
197
|
|
|
168
|
-
// =============================================================================
|
|
169
|
-
// Darwin (macOS) Implementation
|
|
170
|
-
// =============================================================================
|
|
171
|
-
|
|
172
198
|
class DarwinPlatformService extends BasePlatformService {
|
|
173
199
|
getPlatformInfo(): PlatformInfo {
|
|
174
200
|
if (this.cachedPlatformInfo) return this.cachedPlatformInfo
|
|
175
201
|
|
|
176
202
|
const sudoUser = process.env.SUDO_USER || null
|
|
177
|
-
let homeDir: string
|
|
178
203
|
|
|
204
|
+
// Try to get home from getent passwd (may fail on macOS)
|
|
205
|
+
let getentResult: string | null = null
|
|
179
206
|
if (sudoUser) {
|
|
180
|
-
// Running under sudo - get original user's home
|
|
181
207
|
try {
|
|
182
|
-
|
|
208
|
+
getentResult = execSync(`getent passwd ${sudoUser}`, {
|
|
183
209
|
encoding: 'utf-8',
|
|
184
210
|
})
|
|
185
|
-
const parts = result.trim().split(':')
|
|
186
|
-
homeDir =
|
|
187
|
-
parts.length >= 6 && parts[5] ? parts[5] : `/Users/${sudoUser}`
|
|
188
211
|
} catch {
|
|
189
|
-
|
|
212
|
+
// getent may not be available on macOS
|
|
190
213
|
}
|
|
191
|
-
} else {
|
|
192
|
-
homeDir = homedir()
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
const homeDir = resolveHomeDir({
|
|
217
|
+
sudoUser,
|
|
218
|
+
getentResult,
|
|
219
|
+
platform: 'darwin',
|
|
220
|
+
defaultHome: homedir(),
|
|
221
|
+
})
|
|
222
|
+
|
|
195
223
|
this.cachedPlatformInfo = {
|
|
196
224
|
platform: 'darwin',
|
|
197
225
|
arch: osArch() as Architecture,
|
|
@@ -309,31 +337,31 @@ class DarwinPlatformService extends BasePlatformService {
|
|
|
309
337
|
}
|
|
310
338
|
}
|
|
311
339
|
|
|
312
|
-
// =============================================================================
|
|
313
|
-
// Linux Implementation
|
|
314
|
-
// =============================================================================
|
|
315
|
-
|
|
316
340
|
class LinuxPlatformService extends BasePlatformService {
|
|
317
341
|
getPlatformInfo(): PlatformInfo {
|
|
318
342
|
if (this.cachedPlatformInfo) return this.cachedPlatformInfo
|
|
319
343
|
|
|
320
344
|
const sudoUser = process.env.SUDO_USER || null
|
|
321
|
-
let homeDir: string
|
|
322
345
|
|
|
346
|
+
// Try to get home from getent passwd
|
|
347
|
+
let getentResult: string | null = null
|
|
323
348
|
if (sudoUser) {
|
|
324
349
|
try {
|
|
325
|
-
|
|
350
|
+
getentResult = execSync(`getent passwd ${sudoUser}`, {
|
|
326
351
|
encoding: 'utf-8',
|
|
327
352
|
})
|
|
328
|
-
const parts = result.trim().split(':')
|
|
329
|
-
homeDir = parts.length >= 6 && parts[5] ? parts[5] : `/home/${sudoUser}`
|
|
330
353
|
} catch {
|
|
331
|
-
|
|
354
|
+
// getent failed
|
|
332
355
|
}
|
|
333
|
-
} else {
|
|
334
|
-
homeDir = homedir()
|
|
335
356
|
}
|
|
336
357
|
|
|
358
|
+
const homeDir = resolveHomeDir({
|
|
359
|
+
sudoUser,
|
|
360
|
+
getentResult,
|
|
361
|
+
platform: 'linux',
|
|
362
|
+
defaultHome: homedir(),
|
|
363
|
+
})
|
|
364
|
+
|
|
337
365
|
// Check if running in WSL
|
|
338
366
|
let isWSL = false
|
|
339
367
|
try {
|
|
@@ -494,10 +522,6 @@ class LinuxPlatformService extends BasePlatformService {
|
|
|
494
522
|
}
|
|
495
523
|
}
|
|
496
524
|
|
|
497
|
-
// =============================================================================
|
|
498
|
-
// Windows Implementation (Stub for future support)
|
|
499
|
-
// =============================================================================
|
|
500
|
-
|
|
501
525
|
class Win32PlatformService extends BasePlatformService {
|
|
502
526
|
getPlatformInfo(): PlatformInfo {
|
|
503
527
|
if (this.cachedPlatformInfo) return this.cachedPlatformInfo
|
|
@@ -608,10 +632,6 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
608
632
|
}
|
|
609
633
|
}
|
|
610
634
|
|
|
611
|
-
// =============================================================================
|
|
612
|
-
// Factory and Singleton
|
|
613
|
-
// =============================================================================
|
|
614
|
-
|
|
615
635
|
/**
|
|
616
636
|
* Create the appropriate platform service for the current OS
|
|
617
637
|
*/
|
package/core/start-with-retry.ts
CHANGED
|
@@ -9,12 +9,9 @@ import { portManager } from './port-manager'
|
|
|
9
9
|
import { containerManager } from './container-manager'
|
|
10
10
|
import { logWarning, logDebug } from './error-handler'
|
|
11
11
|
import type { BaseEngine } from '../engines/base-engine'
|
|
12
|
+
import { getEngineDefaults } from '../config/defaults'
|
|
12
13
|
import type { ContainerConfig } from '../types'
|
|
13
14
|
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// Types
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
18
15
|
export type StartWithRetryOptions = {
|
|
19
16
|
engine: BaseEngine
|
|
20
17
|
config: ContainerConfig
|
|
@@ -29,13 +26,6 @@ export type StartWithRetryResult = {
|
|
|
29
26
|
error?: Error
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
// =============================================================================
|
|
33
|
-
// Port Error Detection
|
|
34
|
-
// =============================================================================
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Check if an error is a port-in-use error
|
|
38
|
-
*/
|
|
39
29
|
function isPortInUseError(err: unknown): boolean {
|
|
40
30
|
const message = (err as Error)?.message?.toLowerCase() || ''
|
|
41
31
|
return (
|
|
@@ -47,10 +37,6 @@ function isPortInUseError(err: unknown): boolean {
|
|
|
47
37
|
)
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
// =============================================================================
|
|
51
|
-
// Start with Retry Implementation
|
|
52
|
-
// =============================================================================
|
|
53
|
-
|
|
54
40
|
/**
|
|
55
41
|
* Start a database container with automatic port retry on conflict
|
|
56
42
|
*
|
|
@@ -129,20 +115,9 @@ export async function startWithRetry(
|
|
|
129
115
|
}
|
|
130
116
|
}
|
|
131
117
|
|
|
132
|
-
/**
|
|
133
|
-
* Get the port range for an engine
|
|
134
|
-
*/
|
|
135
118
|
function getEnginePortRange(engine: string): { start: number; end: number } {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (engine === 'postgresql') {
|
|
139
|
-
return { start: 5432, end: 5500 }
|
|
140
|
-
}
|
|
141
|
-
if (engine === 'mysql') {
|
|
142
|
-
return { start: 3306, end: 3400 }
|
|
143
|
-
}
|
|
144
|
-
// Default fallback range
|
|
145
|
-
return { start: 5432, end: 6000 }
|
|
119
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
120
|
+
return engineDefaults.portRange
|
|
146
121
|
}
|
|
147
122
|
|
|
148
123
|
/**
|
|
@@ -7,19 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { logError, logDebug, ErrorCodes } from './error-handler'
|
|
9
9
|
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// Types
|
|
12
|
-
// =============================================================================
|
|
13
|
-
|
|
14
10
|
export type RollbackAction = {
|
|
15
11
|
description: string
|
|
16
12
|
execute: () => Promise<void>
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
// =============================================================================
|
|
20
|
-
// Transaction Manager Implementation
|
|
21
|
-
// =============================================================================
|
|
22
|
-
|
|
23
15
|
/**
|
|
24
16
|
* Manages a stack of rollback actions for transactional operations.
|
|
25
17
|
*
|
package/engines/base-engine.ts
CHANGED
|
@@ -151,4 +151,14 @@ export abstract class BaseEngine {
|
|
|
151
151
|
outputPath: string,
|
|
152
152
|
options: BackupOptions,
|
|
153
153
|
): Promise<BackupResult>
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run a SQL file or inline SQL statement against the database
|
|
157
|
+
* @param container - The container configuration
|
|
158
|
+
* @param options - Options including file path or SQL statement, and target database
|
|
159
|
+
*/
|
|
160
|
+
abstract runScript(
|
|
161
|
+
container: ContainerConfig,
|
|
162
|
+
options: { file?: string; sql?: string; database?: string },
|
|
163
|
+
): Promise<void>
|
|
154
164
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { exec } from 'child_process'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
7
8
|
import { promisify } from 'util'
|
|
8
9
|
import { platformService } from '../../core/platform-service'
|
|
9
10
|
|
|
@@ -129,7 +130,6 @@ export async function detectInstalledVersions(): Promise<
|
|
|
129
130
|
'/usr/local/opt/mysql@8.4/bin/mysqld',
|
|
130
131
|
]
|
|
131
132
|
|
|
132
|
-
const { existsSync } = await import('fs')
|
|
133
133
|
for (const path of homebrewPaths) {
|
|
134
134
|
if (existsSync(path)) {
|
|
135
135
|
const version = await getMysqlVersion(path)
|
package/engines/mysql/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { spawn, exec } from 'child_process'
|
|
7
7
|
import { promisify } from 'util'
|
|
8
|
-
import { existsSync } from 'fs'
|
|
8
|
+
import { existsSync, createReadStream } from 'fs'
|
|
9
9
|
import { mkdir, writeFile, readFile, unlink } from 'fs/promises'
|
|
10
10
|
import { join } from 'path'
|
|
11
11
|
import { BaseEngine } from '../base-engine'
|
|
@@ -289,8 +289,9 @@ export class MySQLEngine extends BaseEngine {
|
|
|
289
289
|
// Write PID file manually since we're running detached
|
|
290
290
|
try {
|
|
291
291
|
await writeFile(pidFile, String(proc.pid))
|
|
292
|
-
} catch {
|
|
292
|
+
} catch (error) {
|
|
293
293
|
// PID file might be written by mysqld itself
|
|
294
|
+
logDebug(`Could not write PID file (mysqld may write it): ${error}`)
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
// Wait for MySQL to be ready
|
|
@@ -842,6 +843,81 @@ export class MySQLEngine extends BaseEngine {
|
|
|
842
843
|
): Promise<BackupResult> {
|
|
843
844
|
return createBackup(container, outputPath, options)
|
|
844
845
|
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Run a SQL file or inline SQL statement against the database
|
|
849
|
+
* CLI wrapper: mysql -h 127.0.0.1 -P {port} -u root {db} < {file}
|
|
850
|
+
* CLI wrapper: mysql -h 127.0.0.1 -P {port} -u root {db} -e "{sql}"
|
|
851
|
+
*/
|
|
852
|
+
async runScript(
|
|
853
|
+
container: ContainerConfig,
|
|
854
|
+
options: { file?: string; sql?: string; database?: string },
|
|
855
|
+
): Promise<void> {
|
|
856
|
+
const { port } = container
|
|
857
|
+
const db = options.database || container.database || 'mysql'
|
|
858
|
+
|
|
859
|
+
const mysql = await getMysqlClientPath()
|
|
860
|
+
if (!mysql) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
'mysql client not found. Install MySQL client tools:\n' +
|
|
863
|
+
' macOS: brew install mysql-client\n' +
|
|
864
|
+
' Ubuntu/Debian: sudo apt install mysql-client',
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const args = [
|
|
869
|
+
'-h',
|
|
870
|
+
'127.0.0.1',
|
|
871
|
+
'-P',
|
|
872
|
+
String(port),
|
|
873
|
+
'-u',
|
|
874
|
+
engineDef.superuser,
|
|
875
|
+
db,
|
|
876
|
+
]
|
|
877
|
+
|
|
878
|
+
if (options.sql) {
|
|
879
|
+
// For inline SQL, use -e flag
|
|
880
|
+
args.push('-e', options.sql)
|
|
881
|
+
return new Promise((resolve, reject) => {
|
|
882
|
+
const proc = spawn(mysql, args, { stdio: 'inherit' })
|
|
883
|
+
|
|
884
|
+
proc.on('error', reject)
|
|
885
|
+
proc.on('close', (code) => {
|
|
886
|
+
if (code === 0) {
|
|
887
|
+
resolve()
|
|
888
|
+
} else {
|
|
889
|
+
reject(new Error(`mysql exited with code ${code}`))
|
|
890
|
+
}
|
|
891
|
+
})
|
|
892
|
+
})
|
|
893
|
+
} else if (options.file) {
|
|
894
|
+
// For file input, pipe the file to mysql stdin
|
|
895
|
+
return new Promise((resolve, reject) => {
|
|
896
|
+
const fileStream = createReadStream(options.file!)
|
|
897
|
+
const proc = spawn(mysql, args, {
|
|
898
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
fileStream.pipe(proc.stdin)
|
|
902
|
+
|
|
903
|
+
fileStream.on('error', (err) => {
|
|
904
|
+
proc.kill()
|
|
905
|
+
reject(err)
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
proc.on('error', reject)
|
|
909
|
+
proc.on('close', (code) => {
|
|
910
|
+
if (code === 0) {
|
|
911
|
+
resolve()
|
|
912
|
+
} else {
|
|
913
|
+
reject(new Error(`mysql exited with code ${code}`))
|
|
914
|
+
}
|
|
915
|
+
})
|
|
916
|
+
})
|
|
917
|
+
} else {
|
|
918
|
+
throw new Error('Either file or sql option must be provided')
|
|
919
|
+
}
|
|
920
|
+
}
|
|
845
921
|
}
|
|
846
922
|
|
|
847
923
|
export const mysqlEngine = new MySQLEngine()
|