spindb 0.9.0 → 0.9.2
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 +7 -0
- package/cli/commands/backup.ts +13 -11
- package/cli/commands/clone.ts +18 -8
- package/cli/commands/config.ts +29 -29
- package/cli/commands/connect.ts +51 -39
- package/cli/commands/create.ts +120 -43
- package/cli/commands/delete.ts +8 -8
- package/cli/commands/deps.ts +17 -15
- package/cli/commands/doctor.ts +16 -15
- package/cli/commands/edit.ts +115 -60
- package/cli/commands/engines.ts +50 -17
- package/cli/commands/info.ts +12 -8
- package/cli/commands/list.ts +34 -19
- package/cli/commands/logs.ts +24 -14
- package/cli/commands/menu/backup-handlers.ts +72 -49
- package/cli/commands/menu/container-handlers.ts +140 -80
- package/cli/commands/menu/engine-handlers.ts +145 -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 +105 -43
- package/cli/commands/run.ts +20 -18
- package/cli/commands/self-update.ts +5 -5
- 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 +49 -4
- package/cli/ui/prompts.ts +21 -8
- package/cli/ui/spinner.ts +4 -4
- package/cli/ui/theme.ts +4 -4
- package/core/binary-manager.ts +5 -1
- package/core/container-manager.ts +81 -30
- package/core/error-handler.ts +31 -0
- package/core/platform-service.ts +3 -3
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- package/core/start-with-retry.ts +6 -6
- package/core/transaction-manager.ts +6 -6
- package/engines/mysql/backup.ts +53 -36
- package/engines/mysql/index.ts +59 -16
- 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 +13 -2
- package/engines/postgresql/restore.ts +2 -2
- package/engines/postgresql/version-validator.ts +2 -2
- package/engines/sqlite/index.ts +31 -9
- package/package.json +1 -1
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
|
@@ -56,6 +56,20 @@ async function createSqlBackup(
|
|
|
56
56
|
outputPath: string,
|
|
57
57
|
): Promise<BackupResult> {
|
|
58
58
|
return new Promise((resolve, reject) => {
|
|
59
|
+
let settled = false
|
|
60
|
+
const safeResolve = (value: BackupResult) => {
|
|
61
|
+
if (!settled) {
|
|
62
|
+
settled = true
|
|
63
|
+
resolve(value)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const safeReject = (err: Error) => {
|
|
67
|
+
if (!settled) {
|
|
68
|
+
settled = true
|
|
69
|
+
reject(err)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
const args = [
|
|
60
74
|
'-h',
|
|
61
75
|
'127.0.0.1',
|
|
@@ -79,20 +93,20 @@ async function createSqlBackup(
|
|
|
79
93
|
})
|
|
80
94
|
|
|
81
95
|
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
82
|
-
|
|
96
|
+
safeReject(err)
|
|
83
97
|
})
|
|
84
98
|
|
|
85
99
|
proc.on('close', async (code) => {
|
|
86
100
|
if (code === 0) {
|
|
87
101
|
const stats = await stat(outputPath)
|
|
88
|
-
|
|
102
|
+
safeResolve({
|
|
89
103
|
path: outputPath,
|
|
90
104
|
format: 'sql',
|
|
91
105
|
size: stats.size,
|
|
92
106
|
})
|
|
93
107
|
} else {
|
|
94
108
|
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
95
|
-
|
|
109
|
+
safeReject(new Error(errorMessage))
|
|
96
110
|
}
|
|
97
111
|
})
|
|
98
112
|
})
|
|
@@ -108,52 +122,55 @@ async function createCompressedBackup(
|
|
|
108
122
|
database: string,
|
|
109
123
|
outputPath: string,
|
|
110
124
|
): Promise<BackupResult> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})
|
|
125
|
+
const args = [
|
|
126
|
+
'-h',
|
|
127
|
+
'127.0.0.1',
|
|
128
|
+
'-P',
|
|
129
|
+
String(port),
|
|
130
|
+
'-u',
|
|
131
|
+
engineDef.superuser,
|
|
132
|
+
database,
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
const proc = spawn(mysqldump, args, {
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
|
+
})
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
const gzip = createGzip()
|
|
140
|
+
const output = createWriteStream(outputPath)
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
let stderr = ''
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
145
|
+
stderr += data.toString()
|
|
146
|
+
})
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.then(async () => {
|
|
138
|
-
const stats = await stat(outputPath)
|
|
139
|
-
resolve({
|
|
140
|
-
path: outputPath,
|
|
141
|
-
format: 'compressed',
|
|
142
|
-
size: stats.size,
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
.catch(reject)
|
|
148
|
+
// Create promise for pipeline completion
|
|
149
|
+
const pipelinePromise = pipeline(proc.stdout!, gzip, output)
|
|
146
150
|
|
|
151
|
+
// Create promise for process exit
|
|
152
|
+
const exitPromise = new Promise<void>((resolve, reject) => {
|
|
147
153
|
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
148
154
|
reject(err)
|
|
149
155
|
})
|
|
150
156
|
|
|
151
157
|
proc.on('close', (code) => {
|
|
152
|
-
if (code
|
|
158
|
+
if (code === 0) {
|
|
159
|
+
resolve()
|
|
160
|
+
} else {
|
|
153
161
|
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
154
162
|
reject(new Error(errorMessage))
|
|
155
163
|
}
|
|
156
|
-
// If code is 0, the pipeline promise will resolve
|
|
157
164
|
})
|
|
158
165
|
})
|
|
166
|
+
|
|
167
|
+
// Wait for both pipeline AND process exit to succeed
|
|
168
|
+
await Promise.all([pipelinePromise, exitPromise])
|
|
169
|
+
|
|
170
|
+
const stats = await stat(outputPath)
|
|
171
|
+
return {
|
|
172
|
+
path: outputPath,
|
|
173
|
+
format: 'compressed',
|
|
174
|
+
size: stats.size,
|
|
175
|
+
}
|
|
159
176
|
}
|
package/engines/mysql/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { spawn, exec } from 'child_process'
|
|
7
7
|
import { promisify } from 'util'
|
|
8
8
|
import { existsSync, createReadStream } from 'fs'
|
|
9
|
-
import { mkdir, writeFile, readFile, unlink } from 'fs/promises'
|
|
9
|
+
import { mkdir, writeFile, readFile, unlink, rm } from 'fs/promises'
|
|
10
10
|
import { join } from 'path'
|
|
11
11
|
import { BaseEngine } from '../base-engine'
|
|
12
12
|
import { paths } from '../../config/paths'
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
logWarning,
|
|
17
17
|
ErrorCodes,
|
|
18
18
|
SpinDBError,
|
|
19
|
+
assertValidDatabaseName,
|
|
19
20
|
} from '../../core/error-handler'
|
|
20
21
|
import {
|
|
21
22
|
getMysqldPath,
|
|
@@ -135,9 +136,27 @@ export class MySQLEngine extends BaseEngine {
|
|
|
135
136
|
engine: ENGINE,
|
|
136
137
|
})
|
|
137
138
|
|
|
139
|
+
// Track if we created the directory (for cleanup on failure)
|
|
140
|
+
let createdDataDir = false
|
|
141
|
+
|
|
138
142
|
// Create data directory if it doesn't exist
|
|
139
143
|
if (!existsSync(dataDir)) {
|
|
140
144
|
await mkdir(dataDir, { recursive: true })
|
|
145
|
+
createdDataDir = true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Helper to clean up on failure
|
|
149
|
+
const cleanupOnFailure = async () => {
|
|
150
|
+
if (createdDataDir) {
|
|
151
|
+
try {
|
|
152
|
+
await rm(dataDir, { recursive: true, force: true })
|
|
153
|
+
logDebug(`Cleaned up data directory after init failure: ${dataDir}`)
|
|
154
|
+
} catch (cleanupErr) {
|
|
155
|
+
logDebug(
|
|
156
|
+
`Failed to clean up data directory: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
// Check if we're using MariaDB or MySQL
|
|
@@ -148,6 +167,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
148
167
|
const installDb =
|
|
149
168
|
(await getMariadbInstallDbPath()) || (await getMysqlInstallDbPath())
|
|
150
169
|
if (!installDb) {
|
|
170
|
+
await cleanupOnFailure()
|
|
151
171
|
throw new Error(
|
|
152
172
|
'MariaDB detected but mysql_install_db not found.\n' +
|
|
153
173
|
'Install MariaDB server package which includes the initialization script.',
|
|
@@ -177,10 +197,11 @@ export class MySQLEngine extends BaseEngine {
|
|
|
177
197
|
stderr += data.toString()
|
|
178
198
|
})
|
|
179
199
|
|
|
180
|
-
proc.on('close', (code) => {
|
|
200
|
+
proc.on('close', async (code) => {
|
|
181
201
|
if (code === 0) {
|
|
182
202
|
resolve(dataDir)
|
|
183
203
|
} else {
|
|
204
|
+
await cleanupOnFailure()
|
|
184
205
|
reject(
|
|
185
206
|
new Error(
|
|
186
207
|
`MariaDB initialization failed with code ${code}: ${stderr || stdout}`,
|
|
@@ -189,12 +210,16 @@ export class MySQLEngine extends BaseEngine {
|
|
|
189
210
|
}
|
|
190
211
|
})
|
|
191
212
|
|
|
192
|
-
proc.on('error',
|
|
213
|
+
proc.on('error', async (err) => {
|
|
214
|
+
await cleanupOnFailure()
|
|
215
|
+
reject(err)
|
|
216
|
+
})
|
|
193
217
|
})
|
|
194
218
|
} else {
|
|
195
219
|
// MySQL uses mysqld --initialize-insecure
|
|
196
220
|
const mysqld = await getMysqldPath()
|
|
197
221
|
if (!mysqld) {
|
|
222
|
+
await cleanupOnFailure()
|
|
198
223
|
throw new Error(getInstallInstructions())
|
|
199
224
|
}
|
|
200
225
|
|
|
@@ -221,10 +246,11 @@ export class MySQLEngine extends BaseEngine {
|
|
|
221
246
|
stderr += data.toString()
|
|
222
247
|
})
|
|
223
248
|
|
|
224
|
-
proc.on('close', (code) => {
|
|
249
|
+
proc.on('close', async (code) => {
|
|
225
250
|
if (code === 0) {
|
|
226
251
|
resolve(dataDir)
|
|
227
252
|
} else {
|
|
253
|
+
await cleanupOnFailure()
|
|
228
254
|
reject(
|
|
229
255
|
new Error(
|
|
230
256
|
`MySQL initialization failed with code ${code}: ${stderr || stdout}`,
|
|
@@ -233,7 +259,10 @@ export class MySQLEngine extends BaseEngine {
|
|
|
233
259
|
}
|
|
234
260
|
})
|
|
235
261
|
|
|
236
|
-
proc.on('error',
|
|
262
|
+
proc.on('error', async (err) => {
|
|
263
|
+
await cleanupOnFailure()
|
|
264
|
+
reject(err)
|
|
265
|
+
})
|
|
237
266
|
})
|
|
238
267
|
}
|
|
239
268
|
}
|
|
@@ -313,6 +342,14 @@ export class MySQLEngine extends BaseEngine {
|
|
|
313
342
|
connectionString: this.getConnectionString(container),
|
|
314
343
|
})
|
|
315
344
|
return
|
|
345
|
+
} else {
|
|
346
|
+
// mysqladmin not found - cannot verify MySQL is ready
|
|
347
|
+
reject(
|
|
348
|
+
new Error(
|
|
349
|
+
'mysqladmin not found - cannot verify MySQL startup. Install MySQL client tools.',
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
return
|
|
316
353
|
}
|
|
317
354
|
} catch {
|
|
318
355
|
if (attempts < maxAttempts) {
|
|
@@ -413,8 +450,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
413
450
|
await this.cleanupPidFile(pidFile)
|
|
414
451
|
return null
|
|
415
452
|
}
|
|
416
|
-
} catch (
|
|
417
|
-
const e =
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const e = error as NodeJS.ErrnoException
|
|
418
455
|
if (e.code !== 'ENOENT') {
|
|
419
456
|
logWarning(`Failed to read PID file: ${e.message}`, {
|
|
420
457
|
pidFile,
|
|
@@ -442,8 +479,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
442
479
|
`"${mysqladmin}" -h 127.0.0.1 -P ${port} -u root shutdown`,
|
|
443
480
|
{ timeout: 5000 },
|
|
444
481
|
)
|
|
445
|
-
} catch (
|
|
446
|
-
const e =
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const e = error as Error
|
|
447
484
|
logDebug(`mysqladmin shutdown failed: ${e.message}`)
|
|
448
485
|
// Continue to wait for process to die or send SIGTERM
|
|
449
486
|
}
|
|
@@ -505,8 +542,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
505
542
|
await this.cleanupPidFile(pidFile)
|
|
506
543
|
return
|
|
507
544
|
}
|
|
508
|
-
} catch (
|
|
509
|
-
const e =
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const e = error as NodeJS.ErrnoException
|
|
510
547
|
if (e.code === 'ESRCH') {
|
|
511
548
|
// Process already dead
|
|
512
549
|
await this.cleanupPidFile(pidFile)
|
|
@@ -538,9 +575,9 @@ export class MySQLEngine extends BaseEngine {
|
|
|
538
575
|
logDebug(`Process ${pid} terminated after SIGKILL`)
|
|
539
576
|
await this.cleanupPidFile(pidFile)
|
|
540
577
|
}
|
|
541
|
-
} catch (
|
|
542
|
-
if (
|
|
543
|
-
const e =
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error instanceof SpinDBError) throw error
|
|
580
|
+
const e = error as NodeJS.ErrnoException
|
|
544
581
|
if (e.code === 'ESRCH') {
|
|
545
582
|
// Process already dead
|
|
546
583
|
await this.cleanupPidFile(pidFile)
|
|
@@ -557,8 +594,8 @@ export class MySQLEngine extends BaseEngine {
|
|
|
557
594
|
try {
|
|
558
595
|
await unlink(pidFile)
|
|
559
596
|
logDebug('PID file cleaned up')
|
|
560
|
-
} catch (
|
|
561
|
-
const e =
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const e = error as NodeJS.ErrnoException
|
|
562
599
|
if (e.code !== 'ENOENT') {
|
|
563
600
|
logDebug(`Failed to clean up PID file: ${e.message}`)
|
|
564
601
|
}
|
|
@@ -687,6 +724,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
687
724
|
container: ContainerConfig,
|
|
688
725
|
database: string,
|
|
689
726
|
): Promise<void> {
|
|
727
|
+
assertValidDatabaseName(database)
|
|
690
728
|
const { port } = container
|
|
691
729
|
|
|
692
730
|
const mysql = await getMysqlClientPath()
|
|
@@ -720,6 +758,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
720
758
|
container: ContainerConfig,
|
|
721
759
|
database: string,
|
|
722
760
|
): Promise<void> {
|
|
761
|
+
assertValidDatabaseName(database)
|
|
723
762
|
const { port } = container
|
|
724
763
|
|
|
725
764
|
const mysql = await getMysqlClientPath()
|
|
@@ -748,6 +787,9 @@ export class MySQLEngine extends BaseEngine {
|
|
|
748
787
|
const { port, database } = container
|
|
749
788
|
const db = database || 'mysql'
|
|
750
789
|
|
|
790
|
+
// Validate database name to prevent SQL injection
|
|
791
|
+
assertValidDatabaseName(db)
|
|
792
|
+
|
|
751
793
|
try {
|
|
752
794
|
const mysql = await getMysqlClientPath()
|
|
753
795
|
if (!mysql) return null
|
|
@@ -856,6 +898,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
856
898
|
): Promise<void> {
|
|
857
899
|
const { port } = container
|
|
858
900
|
const db = options.database || container.database || 'mysql'
|
|
901
|
+
assertValidDatabaseName(db)
|
|
859
902
|
|
|
860
903
|
const mysql = await getMysqlClientPath()
|
|
861
904
|
if (!mysql) {
|
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
|
}
|
|
@@ -2,7 +2,7 @@ import { exec } from 'child_process'
|
|
|
2
2
|
import { promisify } from 'util'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { createSpinner } from '../../cli/ui/spinner'
|
|
5
|
-
import {
|
|
5
|
+
import { uiWarning, uiError, uiSuccess } from '../../cli/ui/theme'
|
|
6
6
|
import {
|
|
7
7
|
detectPackageManager as detectPM,
|
|
8
8
|
installEngineDependencies,
|
|
@@ -311,7 +311,7 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
311
311
|
const packageManager = await detectPM()
|
|
312
312
|
if (!packageManager) {
|
|
313
313
|
spinner.fail('No supported package manager found')
|
|
314
|
-
console.log(
|
|
314
|
+
console.log(uiError('Please install PostgreSQL client tools manually:'))
|
|
315
315
|
|
|
316
316
|
// Show platform-specific instructions from the registry
|
|
317
317
|
const platform = getCurrentPlatform()
|
|
@@ -348,21 +348,21 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
348
348
|
|
|
349
349
|
if (allSuccess) {
|
|
350
350
|
console.log()
|
|
351
|
-
console.log(
|
|
351
|
+
console.log(uiSuccess('PostgreSQL client tools installed successfully'))
|
|
352
352
|
return true
|
|
353
353
|
} else {
|
|
354
354
|
const failed = results.filter((r) => !r.success)
|
|
355
355
|
console.log()
|
|
356
|
-
console.log(
|
|
356
|
+
console.log(uiError('Some installations failed:'))
|
|
357
357
|
for (const f of failed) {
|
|
358
|
-
console.log(
|
|
358
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
359
359
|
}
|
|
360
360
|
return false
|
|
361
361
|
}
|
|
362
362
|
} catch (error: unknown) {
|
|
363
363
|
console.log()
|
|
364
|
-
console.log(
|
|
365
|
-
console.log(
|
|
364
|
+
console.log(uiError('Failed to install PostgreSQL client tools'))
|
|
365
|
+
console.log(uiWarning('Please install manually'))
|
|
366
366
|
if (error instanceof Error) {
|
|
367
367
|
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
368
368
|
}
|
|
@@ -408,7 +408,7 @@ export async function updatePostgresClientTools(): Promise<boolean> {
|
|
|
408
408
|
|
|
409
409
|
spinner.succeed('PostgreSQL client tools updated')
|
|
410
410
|
console.log(
|
|
411
|
-
|
|
411
|
+
uiSuccess(
|
|
412
412
|
`Client tools successfully linked to PostgreSQL ${latestMajor}`,
|
|
413
413
|
),
|
|
414
414
|
)
|
|
@@ -418,13 +418,13 @@ export async function updatePostgresClientTools(): Promise<boolean> {
|
|
|
418
418
|
// For other package managers, use the standard update
|
|
419
419
|
await execWithTimeout(packageManager.updateCommand('postgresql'), 120000)
|
|
420
420
|
spinner.succeed('PostgreSQL client tools updated')
|
|
421
|
-
console.log(
|
|
421
|
+
console.log(uiSuccess('Update completed successfully'))
|
|
422
422
|
return true
|
|
423
423
|
}
|
|
424
424
|
} catch (error: unknown) {
|
|
425
425
|
spinner.fail('Update failed')
|
|
426
|
-
console.log(
|
|
427
|
-
console.log(
|
|
426
|
+
console.log(uiError('Failed to update PostgreSQL client tools'))
|
|
427
|
+
console.log(uiWarning('Please update manually:'))
|
|
428
428
|
|
|
429
429
|
if (packageManager.name === 'brew') {
|
|
430
430
|
const olderVersions = ['14', '15', '16'].filter((v) => v !== latestMajor)
|
|
@@ -473,12 +473,12 @@ export async function updatePostgresBinaries(): Promise<boolean> {
|
|
|
473
473
|
try {
|
|
474
474
|
await execWithTimeout(packageManager.updateCommand('postgresql'), 120000) // 2 minute timeout
|
|
475
475
|
updateSpinner.succeed('PostgreSQL client tools updated')
|
|
476
|
-
console.log(
|
|
476
|
+
console.log(uiSuccess('Update completed successfully'))
|
|
477
477
|
return true
|
|
478
478
|
} catch (error: unknown) {
|
|
479
479
|
updateSpinner.fail('Update failed')
|
|
480
|
-
console.log(
|
|
481
|
-
console.log(
|
|
480
|
+
console.log(uiError('Failed to update PostgreSQL client tools'))
|
|
481
|
+
console.log(uiWarning('Please update manually:'))
|
|
482
482
|
console.log(` ${packageManager.updateCommand('postgresql')}`)
|
|
483
483
|
if (error instanceof Error) {
|
|
484
484
|
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
@@ -505,7 +505,7 @@ export async function ensurePostgresBinary(
|
|
|
505
505
|
return { success: false, info: null, action: 'install_required' }
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
console.log(
|
|
508
|
+
console.log(uiWarning(`${binary} not found on your system`))
|
|
509
509
|
const success = await installPostgresBinaries()
|
|
510
510
|
if (!success) {
|
|
511
511
|
return { success: false, info: null, action: 'install_failed' }
|
|
@@ -527,13 +527,13 @@ export async function ensurePostgresBinary(
|
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
console.log(
|
|
530
|
-
|
|
530
|
+
uiWarning(
|
|
531
531
|
`Your ${binary} version (${info.version}) is incompatible with the dump file`,
|
|
532
532
|
),
|
|
533
533
|
)
|
|
534
534
|
if (info.requiredVersion) {
|
|
535
535
|
console.log(
|
|
536
|
-
|
|
536
|
+
uiWarning(`Required version: ${info.requiredVersion} or compatible`),
|
|
537
537
|
)
|
|
538
538
|
}
|
|
539
539
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './binary-urls'
|
|
19
19
|
import { detectBackupFormat, restoreBackup } from './restore'
|
|
20
20
|
import { createBackup } from './backup'
|
|
21
|
+
import { assertValidDatabaseName } from '../../core/error-handler'
|
|
21
22
|
import type {
|
|
22
23
|
ContainerConfig,
|
|
23
24
|
ProgressCallback,
|
|
@@ -154,8 +155,13 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
154
155
|
|
|
155
156
|
// Configure max_connections after initdb creates postgresql.conf
|
|
156
157
|
const maxConnections =
|
|
157
|
-
(options.maxConnections as number) ||
|
|
158
|
-
|
|
158
|
+
(options.maxConnections as number) ||
|
|
159
|
+
getEngineDefaults('postgresql').maxConnections
|
|
160
|
+
await this.setConfigValue(
|
|
161
|
+
dataDir,
|
|
162
|
+
'max_connections',
|
|
163
|
+
String(maxConnections),
|
|
164
|
+
)
|
|
159
165
|
|
|
160
166
|
return dataDir
|
|
161
167
|
}
|
|
@@ -395,6 +401,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
395
401
|
container: ContainerConfig,
|
|
396
402
|
database: string,
|
|
397
403
|
): Promise<void> {
|
|
404
|
+
assertValidDatabaseName(database)
|
|
398
405
|
const { port } = container
|
|
399
406
|
const psqlPath = await this.getPsqlPath()
|
|
400
407
|
|
|
@@ -418,6 +425,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
418
425
|
container: ContainerConfig,
|
|
419
426
|
database: string,
|
|
420
427
|
): Promise<void> {
|
|
428
|
+
assertValidDatabaseName(database)
|
|
421
429
|
const { port } = container
|
|
422
430
|
const psqlPath = await this.getPsqlPath()
|
|
423
431
|
|
|
@@ -443,6 +451,9 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
443
451
|
const { port, database } = container
|
|
444
452
|
const db = database || 'postgres'
|
|
445
453
|
|
|
454
|
+
// Validate database name to prevent SQL injection
|
|
455
|
+
assertValidDatabaseName(db)
|
|
456
|
+
|
|
446
457
|
try {
|
|
447
458
|
const psqlPath = await this.getPsqlPath()
|
|
448
459
|
// Query pg_database_size for the specific database
|
|
@@ -219,8 +219,8 @@ export async function restoreBackup(
|
|
|
219
219
|
format: detectedFormat,
|
|
220
220
|
...result,
|
|
221
221
|
}
|
|
222
|
-
} catch (
|
|
223
|
-
const e =
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const e = error as Error & { stdout?: string; stderr?: string }
|
|
224
224
|
// pg_restore often returns non-zero even on partial success
|
|
225
225
|
return {
|
|
226
226
|
format: detectedFormat,
|
|
@@ -135,11 +135,11 @@ export async function parseDumpVersion(
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
} catch (
|
|
138
|
+
} catch (error) {
|
|
139
139
|
logDebug('Failed to parse dump version', {
|
|
140
140
|
dumpPath,
|
|
141
141
|
format,
|
|
142
|
-
error:
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
143
|
})
|
|
144
144
|
}
|
|
145
145
|
|