spindb 0.9.1 → 0.9.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 +5 -8
- package/cli/commands/attach.ts +108 -0
- package/cli/commands/backup.ts +13 -11
- package/cli/commands/clone.ts +14 -10
- package/cli/commands/config.ts +29 -29
- package/cli/commands/connect.ts +51 -39
- package/cli/commands/create.ts +65 -32
- package/cli/commands/delete.ts +8 -8
- package/cli/commands/deps.ts +17 -15
- package/cli/commands/detach.ts +100 -0
- package/cli/commands/doctor.ts +27 -13
- package/cli/commands/edit.ts +120 -57
- package/cli/commands/engines.ts +17 -15
- package/cli/commands/info.ts +8 -6
- package/cli/commands/list.ts +127 -18
- package/cli/commands/logs.ts +15 -11
- package/cli/commands/menu/backup-handlers.ts +52 -47
- package/cli/commands/menu/container-handlers.ts +164 -79
- package/cli/commands/menu/engine-handlers.ts +21 -11
- package/cli/commands/menu/index.ts +4 -4
- package/cli/commands/menu/shell-handlers.ts +34 -31
- package/cli/commands/menu/sql-handlers.ts +22 -16
- package/cli/commands/menu/update-handlers.ts +19 -17
- package/cli/commands/restore.ts +22 -20
- package/cli/commands/run.ts +20 -18
- package/cli/commands/self-update.ts +5 -5
- package/cli/commands/sqlite.ts +247 -0
- package/cli/commands/start.ts +11 -9
- package/cli/commands/stop.ts +9 -9
- package/cli/commands/url.ts +12 -9
- package/cli/helpers.ts +9 -4
- package/cli/index.ts +6 -0
- package/cli/ui/prompts.ts +12 -5
- package/cli/ui/spinner.ts +4 -4
- package/cli/ui/theme.ts +4 -4
- package/config/paths.ts +0 -8
- package/core/binary-manager.ts +5 -1
- package/core/config-manager.ts +32 -0
- package/core/container-manager.ts +5 -5
- package/core/platform-service.ts +3 -3
- package/core/start-with-retry.ts +6 -6
- package/core/transaction-manager.ts +6 -6
- package/engines/mysql/backup.ts +37 -13
- package/engines/mysql/index.ts +11 -11
- package/engines/mysql/restore.ts +4 -4
- package/engines/mysql/version-validator.ts +2 -2
- package/engines/postgresql/binary-manager.ts +17 -17
- package/engines/postgresql/index.ts +7 -2
- package/engines/postgresql/restore.ts +2 -2
- package/engines/postgresql/version-validator.ts +2 -2
- package/engines/sqlite/index.ts +30 -15
- package/engines/sqlite/registry.ts +64 -33
- package/engines/sqlite/scanner.ts +99 -0
- package/package.json +4 -3
- package/types/index.ts +21 -1
package/cli/commands/url.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { containerManager } from '../../core/container-manager'
|
|
|
3
3
|
import { platformService } from '../../core/platform-service'
|
|
4
4
|
import { getEngine } from '../../engines'
|
|
5
5
|
import { promptContainerSelect } from '../ui/prompts'
|
|
6
|
-
import {
|
|
6
|
+
import { uiError, uiWarning, uiSuccess } from '../ui/theme'
|
|
7
7
|
|
|
8
8
|
export const urlCommand = new Command('url')
|
|
9
9
|
.alias('connection-string')
|
|
@@ -24,7 +24,7 @@ export const urlCommand = new Command('url')
|
|
|
24
24
|
const containers = await containerManager.list()
|
|
25
25
|
|
|
26
26
|
if (containers.length === 0) {
|
|
27
|
-
console.log(
|
|
27
|
+
console.log(uiWarning('No containers found'))
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -38,13 +38,16 @@ export const urlCommand = new Command('url')
|
|
|
38
38
|
|
|
39
39
|
const config = await containerManager.getConfig(containerName)
|
|
40
40
|
if (!config) {
|
|
41
|
-
console.error(
|
|
41
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
42
42
|
process.exit(1)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const engine = getEngine(config.engine)
|
|
46
46
|
const databaseName = options.database || config.database
|
|
47
|
-
const connectionString = engine.getConnectionString(
|
|
47
|
+
const connectionString = engine.getConnectionString(
|
|
48
|
+
config,
|
|
49
|
+
databaseName,
|
|
50
|
+
)
|
|
48
51
|
|
|
49
52
|
if (options.json) {
|
|
50
53
|
const jsonOutput =
|
|
@@ -72,10 +75,10 @@ export const urlCommand = new Command('url')
|
|
|
72
75
|
const copied = await platformService.copyToClipboard(connectionString)
|
|
73
76
|
if (copied) {
|
|
74
77
|
console.log(connectionString)
|
|
75
|
-
console.error(
|
|
78
|
+
console.error(uiSuccess('Copied to clipboard'))
|
|
76
79
|
} else {
|
|
77
80
|
console.log(connectionString)
|
|
78
|
-
console.error(
|
|
81
|
+
console.error(uiWarning('Could not copy to clipboard'))
|
|
79
82
|
}
|
|
80
83
|
} else {
|
|
81
84
|
process.stdout.write(connectionString)
|
|
@@ -83,9 +86,9 @@ export const urlCommand = new Command('url')
|
|
|
83
86
|
console.log()
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
|
-
} catch (
|
|
87
|
-
const e =
|
|
88
|
-
console.error(
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const e = error as Error
|
|
91
|
+
console.error(uiError(e.message))
|
|
89
92
|
process.exit(1)
|
|
90
93
|
}
|
|
91
94
|
},
|
package/cli/helpers.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs'
|
|
2
2
|
import { readdir, lstat } from 'fs/promises'
|
|
3
3
|
import { join } from 'path'
|
|
4
|
-
import { exec } from 'child_process'
|
|
4
|
+
import { exec, execFile } from 'child_process'
|
|
5
5
|
import { promisify } from 'util'
|
|
6
6
|
import { paths } from '../config/paths'
|
|
7
7
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../engines/mysql/binary-detection'
|
|
12
12
|
|
|
13
13
|
const execAsync = promisify(exec)
|
|
14
|
+
const execFileAsync = promisify(execFile)
|
|
14
15
|
|
|
15
16
|
export type InstalledPostgresEngine = {
|
|
16
17
|
engine: 'postgresql'
|
|
@@ -49,7 +50,7 @@ async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
try {
|
|
52
|
-
const { stdout } = await
|
|
53
|
+
const { stdout } = await execFileAsync(postgresPath, ['--version'])
|
|
53
54
|
const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
|
|
54
55
|
return match ? match[1] : null
|
|
55
56
|
} catch {
|
|
@@ -57,7 +58,9 @@ async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
export async function getInstalledPostgresEngines(): Promise<
|
|
61
|
+
export async function getInstalledPostgresEngines(): Promise<
|
|
62
|
+
InstalledPostgresEngine[]
|
|
63
|
+
> {
|
|
61
64
|
const binDir = paths.bin
|
|
62
65
|
|
|
63
66
|
if (!existsSync(binDir)) {
|
|
@@ -144,7 +147,9 @@ async function getInstalledSqliteEngine(): Promise<InstalledSqliteEngine | null>
|
|
|
144
147
|
return null
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
const { stdout: versionOutput } = await
|
|
150
|
+
const { stdout: versionOutput } = await execFileAsync(sqlitePath, [
|
|
151
|
+
'--version',
|
|
152
|
+
])
|
|
148
153
|
// sqlite3 --version outputs: "3.43.2 2023-10-10 12:14:04 ..."
|
|
149
154
|
const versionMatch = versionOutput.match(/^([\d.]+)/)
|
|
150
155
|
const version = versionMatch ? versionMatch[1] : 'unknown'
|
package/cli/index.ts
CHANGED
|
@@ -25,6 +25,9 @@ import { versionCommand } from './commands/version'
|
|
|
25
25
|
import { runCommand } from './commands/run'
|
|
26
26
|
import { logsCommand } from './commands/logs'
|
|
27
27
|
import { doctorCommand } from './commands/doctor'
|
|
28
|
+
import { attachCommand } from './commands/attach'
|
|
29
|
+
import { detachCommand } from './commands/detach'
|
|
30
|
+
import { sqliteCommand } from './commands/sqlite'
|
|
28
31
|
import { updateManager } from '../core/update-manager'
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -125,6 +128,9 @@ export async function run(): Promise<void> {
|
|
|
125
128
|
program.addCommand(runCommand)
|
|
126
129
|
program.addCommand(logsCommand)
|
|
127
130
|
program.addCommand(doctorCommand)
|
|
131
|
+
program.addCommand(attachCommand)
|
|
132
|
+
program.addCommand(detachCommand)
|
|
133
|
+
program.addCommand(sqliteCommand)
|
|
128
134
|
|
|
129
135
|
// If no arguments provided, show interactive menu
|
|
130
136
|
if (process.argv.length <= 2) {
|
package/cli/ui/prompts.ts
CHANGED
|
@@ -289,7 +289,9 @@ export async function promptDatabaseName(
|
|
|
289
289
|
engine === 'mysql' ? 'Database (schema) name:' : 'Database name:'
|
|
290
290
|
|
|
291
291
|
// Sanitize the default name to ensure it's valid
|
|
292
|
-
const sanitizedDefault = defaultName
|
|
292
|
+
const sanitizedDefault = defaultName
|
|
293
|
+
? sanitizeDatabaseName(defaultName)
|
|
294
|
+
: undefined
|
|
293
295
|
|
|
294
296
|
const { database } = await inquirer.prompt<{ database: string }>([
|
|
295
297
|
{
|
|
@@ -420,7 +422,11 @@ export async function promptSqlitePath(
|
|
|
420
422
|
): Promise<string | undefined> {
|
|
421
423
|
const defaultPath = `./${containerName}.sqlite`
|
|
422
424
|
|
|
423
|
-
console.log(
|
|
425
|
+
console.log(
|
|
426
|
+
chalk.gray(
|
|
427
|
+
' SQLite databases are stored as files in your project directory.',
|
|
428
|
+
),
|
|
429
|
+
)
|
|
424
430
|
console.log(chalk.gray(` Default: ${defaultPath}`))
|
|
425
431
|
console.log()
|
|
426
432
|
|
|
@@ -496,7 +502,8 @@ export async function promptSqlitePath(
|
|
|
496
502
|
{
|
|
497
503
|
type: 'list',
|
|
498
504
|
name: 'overwrite',
|
|
499
|
-
message:
|
|
505
|
+
message:
|
|
506
|
+
'A file already exists at this location. What would you like to do?',
|
|
500
507
|
choices: [
|
|
501
508
|
{ name: 'Choose a different path', value: 'different' },
|
|
502
509
|
{ name: 'Cancel', value: 'cancel' },
|
|
@@ -702,8 +709,8 @@ export async function promptInstallDependencies(
|
|
|
702
709
|
|
|
703
710
|
return false
|
|
704
711
|
}
|
|
705
|
-
} catch (
|
|
706
|
-
const e =
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const e = error as Error
|
|
707
714
|
console.log()
|
|
708
715
|
console.log(chalk.red(` Installation failed: ${e.message}`))
|
|
709
716
|
console.log()
|
package/cli/ui/spinner.ts
CHANGED
|
@@ -27,10 +27,10 @@ export async function withSpinner<T>(
|
|
|
27
27
|
})
|
|
28
28
|
spinner.succeed()
|
|
29
29
|
return result
|
|
30
|
-
} catch (
|
|
31
|
-
const
|
|
32
|
-
spinner.fail(
|
|
33
|
-
throw
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const e = error as Error
|
|
32
|
+
spinner.fail(e.message)
|
|
33
|
+
throw e
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
package/cli/ui/theme.ts
CHANGED
|
@@ -60,28 +60,28 @@ ${chalk.cyan('└' + line + '┘')}
|
|
|
60
60
|
/**
|
|
61
61
|
* Format a success message
|
|
62
62
|
*/
|
|
63
|
-
export function
|
|
63
|
+
export function uiSuccess(message: string): string {
|
|
64
64
|
return `${theme.icons.success} ${message}`
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Format an error message
|
|
69
69
|
*/
|
|
70
|
-
export function
|
|
70
|
+
export function uiError(message: string): string {
|
|
71
71
|
return `${theme.icons.error} ${chalk.red(message)}`
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Format a warning message
|
|
76
76
|
*/
|
|
77
|
-
export function
|
|
77
|
+
export function uiWarning(message: string): string {
|
|
78
78
|
return `${theme.icons.warning} ${chalk.yellow(message)}`
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Format an info message
|
|
83
83
|
*/
|
|
84
|
-
export function
|
|
84
|
+
export function uiInfo(message: string): string {
|
|
85
85
|
return `${theme.icons.info} ${message}`
|
|
86
86
|
}
|
|
87
87
|
|
package/config/paths.ts
CHANGED
|
@@ -114,12 +114,4 @@ export const paths = {
|
|
|
114
114
|
getEngineContainersPath(engine: string): string {
|
|
115
115
|
return join(this.containers, engine)
|
|
116
116
|
},
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get path for SQLite registry file
|
|
120
|
-
* SQLite uses a registry (not container directories) since databases are stored externally
|
|
121
|
-
*/
|
|
122
|
-
getSqliteRegistryPath(): string {
|
|
123
|
-
return join(this.root, 'sqlite-registry.json')
|
|
124
|
-
},
|
|
125
117
|
}
|
package/core/binary-manager.ts
CHANGED
|
@@ -6,7 +6,11 @@ import { exec } from 'child_process'
|
|
|
6
6
|
import { promisify } from 'util'
|
|
7
7
|
import { paths } from '../config/paths'
|
|
8
8
|
import { defaults } from '../config/defaults'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type Engine,
|
|
11
|
+
type ProgressCallback,
|
|
12
|
+
type InstalledBinary,
|
|
13
|
+
} from '../types'
|
|
10
14
|
|
|
11
15
|
const execAsync = promisify(exec)
|
|
12
16
|
|
package/core/config-manager.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
BinaryConfig,
|
|
11
11
|
BinaryTool,
|
|
12
12
|
BinarySource,
|
|
13
|
+
SQLiteEngineRegistry,
|
|
13
14
|
} from '../types'
|
|
14
15
|
|
|
15
16
|
const execAsync = promisify(exec)
|
|
@@ -349,6 +350,37 @@ export class ConfigManager {
|
|
|
349
350
|
config.binaries = {}
|
|
350
351
|
await this.save()
|
|
351
352
|
}
|
|
353
|
+
|
|
354
|
+
// ============================================================
|
|
355
|
+
// SQLite Registry Methods
|
|
356
|
+
// ============================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the SQLite registry from config
|
|
360
|
+
* Returns empty registry if none exists
|
|
361
|
+
*/
|
|
362
|
+
async getSqliteRegistry(): Promise<SQLiteEngineRegistry> {
|
|
363
|
+
const config = await this.load()
|
|
364
|
+
return (
|
|
365
|
+
config.registry?.sqlite ?? {
|
|
366
|
+
version: 1,
|
|
367
|
+
entries: [],
|
|
368
|
+
ignoreFolders: {},
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Save the SQLite registry to config
|
|
375
|
+
*/
|
|
376
|
+
async saveSqliteRegistry(registry: SQLiteEngineRegistry): Promise<void> {
|
|
377
|
+
const config = await this.load()
|
|
378
|
+
if (!config.registry) {
|
|
379
|
+
config.registry = {}
|
|
380
|
+
}
|
|
381
|
+
config.registry.sqlite = registry
|
|
382
|
+
await this.save()
|
|
383
|
+
}
|
|
352
384
|
}
|
|
353
385
|
|
|
354
386
|
export const configManager = new ConfigManager()
|
|
@@ -397,12 +397,12 @@ export class ContainerManager {
|
|
|
397
397
|
await this.saveConfig(targetName, { engine }, config)
|
|
398
398
|
|
|
399
399
|
return config
|
|
400
|
-
} catch (
|
|
400
|
+
} catch (error) {
|
|
401
401
|
// Clean up the copied directory on failure
|
|
402
402
|
await rm(targetPath, { recursive: true, force: true }).catch(() => {
|
|
403
403
|
// Ignore cleanup errors
|
|
404
404
|
})
|
|
405
|
-
throw
|
|
405
|
+
throw error
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
|
|
@@ -497,8 +497,8 @@ export class ContainerManager {
|
|
|
497
497
|
try {
|
|
498
498
|
// Try atomic rename first (only works on same filesystem)
|
|
499
499
|
await fsRename(sourcePath, targetPath)
|
|
500
|
-
} catch (
|
|
501
|
-
const e =
|
|
500
|
+
} catch (error) {
|
|
501
|
+
const e = error as NodeJS.ErrnoException
|
|
502
502
|
if (e.code === 'EXDEV') {
|
|
503
503
|
// Cross-filesystem move - fall back to copy+delete
|
|
504
504
|
await cp(sourcePath, targetPath, { recursive: true })
|
|
@@ -514,7 +514,7 @@ export class ContainerManager {
|
|
|
514
514
|
)
|
|
515
515
|
}
|
|
516
516
|
} else {
|
|
517
|
-
throw
|
|
517
|
+
throw error
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
520
|
}
|
package/core/platform-service.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { homedir, platform as osPlatform, arch as osArch } from 'os'
|
|
14
|
-
import { execSync, exec, spawn } from 'child_process'
|
|
14
|
+
import { execSync, execFileSync, exec, spawn } from 'child_process'
|
|
15
15
|
import { promisify } from 'util'
|
|
16
16
|
import { existsSync } from 'fs'
|
|
17
17
|
|
|
@@ -205,7 +205,7 @@ class DarwinPlatformService extends BasePlatformService {
|
|
|
205
205
|
let getentResult: string | null = null
|
|
206
206
|
if (sudoUser) {
|
|
207
207
|
try {
|
|
208
|
-
getentResult =
|
|
208
|
+
getentResult = execFileSync('getent', ['passwd', sudoUser], {
|
|
209
209
|
encoding: 'utf-8',
|
|
210
210
|
})
|
|
211
211
|
} catch {
|
|
@@ -347,7 +347,7 @@ class LinuxPlatformService extends BasePlatformService {
|
|
|
347
347
|
let getentResult: string | null = null
|
|
348
348
|
if (sudoUser) {
|
|
349
349
|
try {
|
|
350
|
-
getentResult =
|
|
350
|
+
getentResult = execFileSync('getent', ['passwd', sudoUser], {
|
|
351
351
|
encoding: 'utf-8',
|
|
352
352
|
})
|
|
353
353
|
} catch {
|
package/core/start-with-retry.ts
CHANGED
|
@@ -26,8 +26,8 @@ export type StartWithRetryResult = {
|
|
|
26
26
|
error?: Error
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function isPortInUseError(
|
|
30
|
-
const message = (
|
|
29
|
+
function isPortInUseError(error: unknown): boolean {
|
|
30
|
+
const message = (error as Error)?.message?.toLowerCase() || ''
|
|
31
31
|
return (
|
|
32
32
|
message.includes('address already in use') ||
|
|
33
33
|
message.includes('eaddrinuse') ||
|
|
@@ -62,14 +62,14 @@ export async function startWithRetry(
|
|
|
62
62
|
finalPort: config.port,
|
|
63
63
|
retriesUsed: attempt - 1,
|
|
64
64
|
}
|
|
65
|
-
} catch (
|
|
66
|
-
const isPortError = isPortInUseError(
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const isPortError = isPortInUseError(error)
|
|
67
67
|
|
|
68
68
|
logDebug(`Start attempt ${attempt} failed`, {
|
|
69
69
|
containerName: config.name,
|
|
70
70
|
port: config.port,
|
|
71
71
|
isPortError,
|
|
72
|
-
error:
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
if (isPortError && attempt < maxRetries) {
|
|
@@ -101,7 +101,7 @@ export async function startWithRetry(
|
|
|
101
101
|
success: false,
|
|
102
102
|
finalPort: config.port,
|
|
103
103
|
retriesUsed: attempt - 1,
|
|
104
|
-
error:
|
|
104
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -36,9 +36,9 @@ export type RollbackAction = {
|
|
|
36
36
|
* })
|
|
37
37
|
*
|
|
38
38
|
* tx.commit() // Success - clear rollback stack
|
|
39
|
-
* } catch (
|
|
39
|
+
* } catch (error) {
|
|
40
40
|
* await tx.rollback() // Error - undo everything
|
|
41
|
-
* throw
|
|
41
|
+
* throw error
|
|
42
42
|
* }
|
|
43
43
|
* ```
|
|
44
44
|
*/
|
|
@@ -85,14 +85,14 @@ export class TransactionManager {
|
|
|
85
85
|
logDebug(`Executing rollback: ${action.description}`)
|
|
86
86
|
await action.execute()
|
|
87
87
|
logDebug(`Rollback successful: ${action.description}`)
|
|
88
|
-
} catch (
|
|
88
|
+
} catch (error) {
|
|
89
89
|
// Log error but continue with other rollbacks
|
|
90
90
|
logError({
|
|
91
91
|
code: ErrorCodes.ROLLBACK_FAILED,
|
|
92
92
|
message: `Failed to rollback: ${action.description}`,
|
|
93
93
|
severity: 'warning',
|
|
94
94
|
context: {
|
|
95
|
-
error:
|
|
95
|
+
error: error instanceof Error ? error.message : String(error),
|
|
96
96
|
},
|
|
97
97
|
})
|
|
98
98
|
}
|
|
@@ -155,8 +155,8 @@ export async function withTransaction<T>(
|
|
|
155
155
|
const result = await operation(tx)
|
|
156
156
|
tx.commit()
|
|
157
157
|
return result
|
|
158
|
-
} catch (
|
|
158
|
+
} catch (error) {
|
|
159
159
|
await tx.rollback()
|
|
160
|
-
throw
|
|
160
|
+
throw error
|
|
161
161
|
}
|
|
162
162
|
}
|
package/engines/mysql/backup.ts
CHANGED
|
@@ -98,12 +98,20 @@ async function createSqlBackup(
|
|
|
98
98
|
|
|
99
99
|
proc.on('close', async (code) => {
|
|
100
100
|
if (code === 0) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
try {
|
|
102
|
+
const stats = await stat(outputPath)
|
|
103
|
+
safeResolve({
|
|
104
|
+
path: outputPath,
|
|
105
|
+
format: 'sql',
|
|
106
|
+
size: stats.size,
|
|
107
|
+
})
|
|
108
|
+
} catch (error) {
|
|
109
|
+
safeReject(
|
|
110
|
+
new Error(
|
|
111
|
+
`Backup completed but failed to read output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
107
115
|
} else {
|
|
108
116
|
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
109
117
|
safeReject(new Error(errorMessage))
|
|
@@ -164,13 +172,29 @@ async function createCompressedBackup(
|
|
|
164
172
|
})
|
|
165
173
|
})
|
|
166
174
|
|
|
167
|
-
// Wait for both pipeline AND process exit to
|
|
168
|
-
|
|
175
|
+
// Wait for both pipeline AND process exit to complete
|
|
176
|
+
// Use allSettled to handle case where both reject (avoids unhandled rejection)
|
|
177
|
+
const results = await Promise.allSettled([pipelinePromise, exitPromise])
|
|
169
178
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
// Check for any rejections - prefer exitPromise error as it has more context
|
|
180
|
+
const [pipelineResult, exitResult] = results
|
|
181
|
+
if (exitResult.status === 'rejected') {
|
|
182
|
+
throw exitResult.reason
|
|
183
|
+
}
|
|
184
|
+
if (pipelineResult.status === 'rejected') {
|
|
185
|
+
throw pipelineResult.reason
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const stats = await stat(outputPath)
|
|
190
|
+
return {
|
|
191
|
+
path: outputPath,
|
|
192
|
+
format: 'compressed',
|
|
193
|
+
size: stats.size,
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Backup completed but failed to read output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
198
|
+
)
|
|
175
199
|
}
|
|
176
200
|
}
|
package/engines/mysql/index.ts
CHANGED
|
@@ -450,8 +450,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
450
450
|
await this.cleanupPidFile(pidFile)
|
|
451
451
|
return null
|
|
452
452
|
}
|
|
453
|
-
} catch (
|
|
454
|
-
const e =
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const e = error as NodeJS.ErrnoException
|
|
455
455
|
if (e.code !== 'ENOENT') {
|
|
456
456
|
logWarning(`Failed to read PID file: ${e.message}`, {
|
|
457
457
|
pidFile,
|
|
@@ -479,8 +479,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
479
479
|
`"${mysqladmin}" -h 127.0.0.1 -P ${port} -u root shutdown`,
|
|
480
480
|
{ timeout: 5000 },
|
|
481
481
|
)
|
|
482
|
-
} catch (
|
|
483
|
-
const e =
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const e = error as Error
|
|
484
484
|
logDebug(`mysqladmin shutdown failed: ${e.message}`)
|
|
485
485
|
// Continue to wait for process to die or send SIGTERM
|
|
486
486
|
}
|
|
@@ -542,8 +542,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
542
542
|
await this.cleanupPidFile(pidFile)
|
|
543
543
|
return
|
|
544
544
|
}
|
|
545
|
-
} catch (
|
|
546
|
-
const e =
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const e = error as NodeJS.ErrnoException
|
|
547
547
|
if (e.code === 'ESRCH') {
|
|
548
548
|
// Process already dead
|
|
549
549
|
await this.cleanupPidFile(pidFile)
|
|
@@ -575,9 +575,9 @@ export class MySQLEngine extends BaseEngine {
|
|
|
575
575
|
logDebug(`Process ${pid} terminated after SIGKILL`)
|
|
576
576
|
await this.cleanupPidFile(pidFile)
|
|
577
577
|
}
|
|
578
|
-
} catch (
|
|
579
|
-
if (
|
|
580
|
-
const e =
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error instanceof SpinDBError) throw error
|
|
580
|
+
const e = error as NodeJS.ErrnoException
|
|
581
581
|
if (e.code === 'ESRCH') {
|
|
582
582
|
// Process already dead
|
|
583
583
|
await this.cleanupPidFile(pidFile)
|
|
@@ -594,8 +594,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
594
594
|
try {
|
|
595
595
|
await unlink(pidFile)
|
|
596
596
|
logDebug('PID file cleaned up')
|
|
597
|
-
} catch (
|
|
598
|
-
const e =
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const e = error as NodeJS.ErrnoException
|
|
599
599
|
if (e.code !== 'ENOENT') {
|
|
600
600
|
logDebug(`Failed to clean up PID file: ${e.message}`)
|
|
601
601
|
}
|
package/engines/mysql/restore.ts
CHANGED
|
@@ -174,13 +174,13 @@ export async function restoreBackup(
|
|
|
174
174
|
if (validateVersion) {
|
|
175
175
|
try {
|
|
176
176
|
await validateRestoreCompatibility({ dumpPath: backupPath })
|
|
177
|
-
} catch (
|
|
177
|
+
} catch (error) {
|
|
178
178
|
// Re-throw SpinDBError, log and continue for other errors
|
|
179
|
-
if (
|
|
180
|
-
throw
|
|
179
|
+
if (error instanceof Error && error.name === 'SpinDBError') {
|
|
180
|
+
throw error
|
|
181
181
|
}
|
|
182
182
|
logDebug('Version validation failed, proceeding anyway', {
|
|
183
|
-
error:
|
|
183
|
+
error: error instanceof Error ? error.message : String(error),
|
|
184
184
|
})
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -197,10 +197,10 @@ export async function parseDumpVersion(dumpPath: string): Promise<DumpInfo> {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
return { version: null, variant }
|
|
200
|
-
} catch (
|
|
200
|
+
} catch (error) {
|
|
201
201
|
logDebug('Failed to parse dump version', {
|
|
202
202
|
dumpPath,
|
|
203
|
-
error:
|
|
203
|
+
error: error instanceof Error ? error.message : String(error),
|
|
204
204
|
})
|
|
205
205
|
return { version: null, variant: 'unknown' }
|
|
206
206
|
}
|