spindb 0.4.1 → 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 +207 -101
- package/cli/commands/clone.ts +3 -1
- package/cli/commands/connect.ts +54 -24
- package/cli/commands/create.ts +309 -189
- package/cli/commands/delete.ts +3 -1
- 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/list.ts +14 -3
- package/cli/commands/menu.ts +510 -198
- package/cli/commands/restore.ts +66 -43
- package/cli/commands/start.ts +50 -19
- package/cli/commands/stop.ts +3 -1
- package/cli/commands/url.ts +79 -0
- package/cli/index.ts +9 -3
- package/cli/ui/prompts.ts +99 -34
- package/config/defaults.ts +40 -15
- package/config/engine-defaults.ts +107 -0
- package/config/os-dependencies.ts +119 -124
- package/config/paths.ts +82 -56
- package/core/binary-manager.ts +44 -6
- package/core/config-manager.ts +17 -5
- package/core/container-manager.ts +124 -60
- 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 +51 -32
- package/core/process-manager.ts +26 -8
- package/core/start-with-retry.ts +167 -0
- package/core/transaction-manager.ts +170 -0
- package/engines/index.ts +7 -2
- package/engines/mysql/binary-detection.ts +325 -0
- package/engines/mysql/index.ts +808 -0
- 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 +17 -9
- package/engines/postgresql/restore.ts +54 -5
- package/engines/postgresql/version-validator.ts +262 -0
- package/package.json +9 -3
- package/types/index.ts +29 -5
- package/cli/commands/postgres-tools.ts +0 -216
package/cli/commands/create.ts
CHANGED
|
@@ -5,51 +5,57 @@ import chalk from 'chalk'
|
|
|
5
5
|
import { containerManager } from '../../core/container-manager'
|
|
6
6
|
import { portManager } from '../../core/port-manager'
|
|
7
7
|
import { getEngine } from '../../engines'
|
|
8
|
-
import {
|
|
8
|
+
import { getEngineDefaults } from '../../config/defaults'
|
|
9
9
|
import {
|
|
10
10
|
promptCreateOptions,
|
|
11
11
|
promptInstallDependencies,
|
|
12
12
|
promptContainerName,
|
|
13
|
+
promptConfirm,
|
|
13
14
|
} from '../ui/prompts'
|
|
14
15
|
import { createSpinner } from '../ui/spinner'
|
|
15
16
|
import { header, error, connectionBox } from '../ui/theme'
|
|
16
17
|
import { tmpdir } from 'os'
|
|
17
18
|
import { join } from 'path'
|
|
18
|
-
import { spawn } from 'child_process'
|
|
19
|
-
import { platform } from 'os'
|
|
20
19
|
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
20
|
+
import { platformService } from '../../core/platform-service'
|
|
21
|
+
import { startWithRetry } from '../../core/start-with-retry'
|
|
22
|
+
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
|
+
import type { EngineName } from '../../types'
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Detect if a location string is a connection string or a file path
|
|
27
|
+
* Also infers engine from connection string scheme
|
|
24
28
|
*/
|
|
25
|
-
function detectLocationType(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
function detectLocationType(location: string): {
|
|
30
|
+
type: 'connection' | 'file' | 'not_found'
|
|
31
|
+
inferredEngine?: EngineName
|
|
32
|
+
} {
|
|
33
|
+
// Check for PostgreSQL connection string
|
|
29
34
|
if (
|
|
30
35
|
location.startsWith('postgresql://') ||
|
|
31
36
|
location.startsWith('postgres://')
|
|
32
37
|
) {
|
|
33
|
-
return 'connection'
|
|
38
|
+
return { type: 'connection', inferredEngine: 'postgresql' }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for MySQL connection string
|
|
42
|
+
if (location.startsWith('mysql://')) {
|
|
43
|
+
return { type: 'connection', inferredEngine: 'mysql' }
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
// Check if file exists
|
|
37
47
|
if (existsSync(location)) {
|
|
38
|
-
return 'file'
|
|
48
|
+
return { type: 'file' }
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
return 'not_found'
|
|
51
|
+
return { type: 'not_found' }
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
export const createCommand = new Command('create')
|
|
45
55
|
.description('Create a new database container')
|
|
46
56
|
.argument('[name]', 'Container name')
|
|
47
|
-
.option('-e, --engine <engine>', 'Database engine
|
|
48
|
-
.option(
|
|
49
|
-
'--pg-version <version>',
|
|
50
|
-
'PostgreSQL version',
|
|
51
|
-
defaults.postgresVersion,
|
|
52
|
-
)
|
|
57
|
+
.option('-e, --engine <engine>', 'Database engine (postgresql, mysql)')
|
|
58
|
+
.option('-v, --version <version>', 'Database version')
|
|
53
59
|
.option('-d, --database <database>', 'Database name')
|
|
54
60
|
.option('-p, --port <port>', 'Port number')
|
|
55
61
|
.option('--no-start', 'Do not start the container after creation')
|
|
@@ -61,8 +67,8 @@ export const createCommand = new Command('create')
|
|
|
61
67
|
async (
|
|
62
68
|
name: string | undefined,
|
|
63
69
|
options: {
|
|
64
|
-
engine
|
|
65
|
-
|
|
70
|
+
engine?: string
|
|
71
|
+
version?: string
|
|
66
72
|
database?: string
|
|
67
73
|
port?: string
|
|
68
74
|
start: boolean
|
|
@@ -73,41 +79,39 @@ export const createCommand = new Command('create')
|
|
|
73
79
|
|
|
74
80
|
try {
|
|
75
81
|
let containerName = name
|
|
76
|
-
let engine = options.engine
|
|
77
|
-
let version = options.
|
|
82
|
+
let engine: EngineName = (options.engine as EngineName) || 'postgresql'
|
|
83
|
+
let version = options.version
|
|
78
84
|
let database = options.database
|
|
79
85
|
|
|
80
|
-
//
|
|
81
|
-
if (!containerName) {
|
|
82
|
-
const answers = await promptCreateOptions()
|
|
83
|
-
containerName = answers.name
|
|
84
|
-
engine = answers.engine
|
|
85
|
-
version = answers.version
|
|
86
|
-
database = answers.database
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Default database name to container name if not specified
|
|
90
|
-
database = database ?? containerName
|
|
91
|
-
|
|
92
|
-
// Validate --from location if provided
|
|
86
|
+
// Validate --from location if provided (before prompts so we can infer engine)
|
|
93
87
|
let restoreLocation: string | null = null
|
|
94
88
|
let restoreType: 'connection' | 'file' | null = null
|
|
95
89
|
|
|
96
90
|
if (options.from) {
|
|
97
|
-
const
|
|
91
|
+
const locationInfo = detectLocationType(options.from)
|
|
98
92
|
|
|
99
|
-
if (
|
|
93
|
+
if (locationInfo.type === 'not_found') {
|
|
100
94
|
console.error(error(`Location not found: ${options.from}`))
|
|
101
95
|
console.log(
|
|
102
96
|
chalk.gray(
|
|
103
|
-
' Provide a valid file path or connection string (postgresql
|
|
97
|
+
' Provide a valid file path or connection string (postgresql://, mysql://)',
|
|
104
98
|
),
|
|
105
99
|
)
|
|
106
100
|
process.exit(1)
|
|
107
101
|
}
|
|
108
102
|
|
|
109
103
|
restoreLocation = options.from
|
|
110
|
-
restoreType =
|
|
104
|
+
restoreType = locationInfo.type
|
|
105
|
+
|
|
106
|
+
// Infer engine from connection string if not explicitly set
|
|
107
|
+
if (!options.engine && locationInfo.inferredEngine) {
|
|
108
|
+
engine = locationInfo.inferredEngine
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.gray(
|
|
111
|
+
` Inferred engine "${engine}" from connection string`,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
111
115
|
|
|
112
116
|
// If using --from, we must start the container
|
|
113
117
|
if (options.start === false) {
|
|
@@ -120,6 +124,26 @@ export const createCommand = new Command('create')
|
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
// Get engine defaults for port range and default version
|
|
128
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
129
|
+
|
|
130
|
+
// Set version to engine default if not specified
|
|
131
|
+
if (!version) {
|
|
132
|
+
version = engineDefaults.defaultVersion
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Interactive mode if no name provided
|
|
136
|
+
if (!containerName) {
|
|
137
|
+
const answers = await promptCreateOptions()
|
|
138
|
+
containerName = answers.name
|
|
139
|
+
engine = answers.engine as EngineName
|
|
140
|
+
version = answers.version
|
|
141
|
+
database = answers.database
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Default database name to container name if not specified
|
|
145
|
+
database = database ?? containerName
|
|
146
|
+
|
|
123
147
|
console.log(header('Creating Database Container'))
|
|
124
148
|
console.log()
|
|
125
149
|
|
|
@@ -178,30 +202,39 @@ export const createCommand = new Command('create')
|
|
|
178
202
|
portSpinner.succeed(`Using port ${port}`)
|
|
179
203
|
} else {
|
|
180
204
|
const { port: foundPort, isDefault } =
|
|
181
|
-
await portManager.findAvailablePort(
|
|
205
|
+
await portManager.findAvailablePort({
|
|
206
|
+
preferredPort: engineDefaults.defaultPort,
|
|
207
|
+
portRange: engineDefaults.portRange,
|
|
208
|
+
})
|
|
182
209
|
port = foundPort
|
|
183
210
|
if (isDefault) {
|
|
184
211
|
portSpinner.succeed(`Using default port ${port}`)
|
|
185
212
|
} else {
|
|
186
|
-
portSpinner.warn(
|
|
213
|
+
portSpinner.warn(
|
|
214
|
+
`Default port ${engineDefaults.defaultPort} is in use, using port ${port}`,
|
|
215
|
+
)
|
|
187
216
|
}
|
|
188
217
|
}
|
|
189
218
|
|
|
190
219
|
// Ensure binaries are available
|
|
191
220
|
const binarySpinner = createSpinner(
|
|
192
|
-
`Checking
|
|
221
|
+
`Checking ${dbEngine.displayName} ${version} binaries...`,
|
|
193
222
|
)
|
|
194
223
|
binarySpinner.start()
|
|
195
224
|
|
|
196
225
|
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
197
226
|
if (isInstalled) {
|
|
198
|
-
binarySpinner.succeed(
|
|
227
|
+
binarySpinner.succeed(
|
|
228
|
+
`${dbEngine.displayName} ${version} binaries ready (cached)`,
|
|
229
|
+
)
|
|
199
230
|
} else {
|
|
200
|
-
binarySpinner.text = `Downloading
|
|
231
|
+
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
201
232
|
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
202
233
|
binarySpinner.text = message
|
|
203
234
|
})
|
|
204
|
-
binarySpinner.succeed(
|
|
235
|
+
binarySpinner.succeed(
|
|
236
|
+
`${dbEngine.displayName} ${version} binaries downloaded`,
|
|
237
|
+
)
|
|
205
238
|
}
|
|
206
239
|
|
|
207
240
|
// Check if container name already exists and prompt for new name if needed
|
|
@@ -212,191 +245,273 @@ export const createCommand = new Command('create')
|
|
|
212
245
|
containerName = await promptContainerName()
|
|
213
246
|
}
|
|
214
247
|
|
|
248
|
+
// Create transaction manager for rollback support
|
|
249
|
+
const tx = new TransactionManager()
|
|
250
|
+
|
|
215
251
|
// Create container
|
|
216
252
|
const createSpinnerInstance = createSpinner('Creating container...')
|
|
217
253
|
createSpinnerInstance.start()
|
|
218
254
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
try {
|
|
256
|
+
await containerManager.create(containerName, {
|
|
257
|
+
engine: dbEngine.name as EngineName,
|
|
258
|
+
version,
|
|
259
|
+
port,
|
|
260
|
+
database,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Register rollback action for container deletion
|
|
264
|
+
tx.addRollback({
|
|
265
|
+
description: `Delete container "${containerName}"`,
|
|
266
|
+
execute: async () => {
|
|
267
|
+
await containerManager.delete(containerName, { force: true })
|
|
268
|
+
},
|
|
269
|
+
})
|
|
225
270
|
|
|
226
|
-
|
|
271
|
+
createSpinnerInstance.succeed('Container created')
|
|
272
|
+
} catch (err) {
|
|
273
|
+
createSpinnerInstance.fail('Failed to create container')
|
|
274
|
+
throw err
|
|
275
|
+
}
|
|
227
276
|
|
|
228
277
|
// Initialize database cluster
|
|
229
278
|
const initSpinner = createSpinner('Initializing database cluster...')
|
|
230
279
|
initSpinner.start()
|
|
231
280
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
281
|
+
try {
|
|
282
|
+
await dbEngine.initDataDir(containerName, version, {
|
|
283
|
+
superuser: engineDefaults.superuser,
|
|
284
|
+
})
|
|
285
|
+
// Note: initDataDir is covered by the container delete rollback
|
|
286
|
+
initSpinner.succeed('Database cluster initialized')
|
|
287
|
+
} catch (err) {
|
|
288
|
+
initSpinner.fail('Failed to initialize database cluster')
|
|
289
|
+
await tx.rollback()
|
|
290
|
+
throw err
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Determine if we should start the container
|
|
294
|
+
// If --from is specified, we must start to restore
|
|
295
|
+
// If --no-start is specified, don't start
|
|
296
|
+
// Otherwise, ask the user
|
|
297
|
+
let shouldStart = false
|
|
298
|
+
if (restoreLocation) {
|
|
299
|
+
// Must start to restore data
|
|
300
|
+
shouldStart = true
|
|
301
|
+
} else if (options.start === false) {
|
|
302
|
+
// User explicitly requested no start
|
|
303
|
+
shouldStart = false
|
|
304
|
+
} else {
|
|
305
|
+
// Ask the user
|
|
306
|
+
console.log()
|
|
307
|
+
shouldStart = await promptConfirm(`Start ${containerName} now?`, true)
|
|
308
|
+
}
|
|
235
309
|
|
|
236
|
-
|
|
310
|
+
// Get container config for starting and restoration
|
|
311
|
+
const config = await containerManager.getConfig(containerName)
|
|
237
312
|
|
|
238
313
|
// Start container if requested
|
|
239
|
-
if (
|
|
240
|
-
const startSpinner = createSpinner(
|
|
314
|
+
if (shouldStart && config) {
|
|
315
|
+
const startSpinner = createSpinner(
|
|
316
|
+
`Starting ${dbEngine.displayName}...`,
|
|
317
|
+
)
|
|
241
318
|
startSpinner.start()
|
|
242
319
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
await
|
|
320
|
+
try {
|
|
321
|
+
// Use startWithRetry to handle port race conditions
|
|
322
|
+
const result = await startWithRetry({
|
|
323
|
+
engine: dbEngine,
|
|
324
|
+
config,
|
|
325
|
+
onPortChange: (oldPort, newPort) => {
|
|
326
|
+
startSpinner.text = `Port ${oldPort} was in use, retrying with port ${newPort}...`
|
|
327
|
+
port = newPort
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
if (!result.success) {
|
|
332
|
+
startSpinner.fail(`Failed to start ${dbEngine.displayName}`)
|
|
333
|
+
await tx.rollback()
|
|
334
|
+
if (result.error) {
|
|
335
|
+
throw result.error
|
|
336
|
+
}
|
|
337
|
+
throw new Error('Failed to start container')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Register rollback action for stopping the container
|
|
341
|
+
tx.addRollback({
|
|
342
|
+
description: `Stop container "${containerName}"`,
|
|
343
|
+
execute: async () => {
|
|
344
|
+
try {
|
|
345
|
+
await dbEngine.stop(config)
|
|
346
|
+
} catch {
|
|
347
|
+
// Ignore stop errors during rollback
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
})
|
|
351
|
+
|
|
246
352
|
await containerManager.updateConfig(containerName, {
|
|
247
353
|
status: 'running',
|
|
248
354
|
})
|
|
249
|
-
}
|
|
250
355
|
|
|
251
|
-
|
|
356
|
+
if (result.retriesUsed > 0) {
|
|
357
|
+
startSpinner.warn(
|
|
358
|
+
`${dbEngine.displayName} started on port ${result.finalPort} (original port was in use)`,
|
|
359
|
+
)
|
|
360
|
+
} else {
|
|
361
|
+
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (!startSpinner.isSpinning) {
|
|
365
|
+
// Error was already handled above
|
|
366
|
+
} else {
|
|
367
|
+
startSpinner.fail(`Failed to start ${dbEngine.displayName}`)
|
|
368
|
+
}
|
|
369
|
+
await tx.rollback()
|
|
370
|
+
throw err
|
|
371
|
+
}
|
|
252
372
|
|
|
253
|
-
// Create the user's database (if different from
|
|
254
|
-
|
|
373
|
+
// Create the user's database (if different from default)
|
|
374
|
+
const defaultDb = engineDefaults.superuser // postgres or root
|
|
375
|
+
if (database !== defaultDb) {
|
|
255
376
|
const dbSpinner = createSpinner(
|
|
256
377
|
`Creating database "${database}"...`,
|
|
257
378
|
)
|
|
258
379
|
dbSpinner.start()
|
|
259
380
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
381
|
+
try {
|
|
382
|
+
await dbEngine.createDatabase(config, database)
|
|
383
|
+
dbSpinner.succeed(`Database "${database}" created`)
|
|
384
|
+
} catch (err) {
|
|
385
|
+
dbSpinner.fail(`Failed to create database "${database}"`)
|
|
386
|
+
await tx.rollback()
|
|
387
|
+
throw err
|
|
388
|
+
}
|
|
263
389
|
}
|
|
390
|
+
}
|
|
264
391
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
392
|
+
// Handle --from restore if specified (only if started)
|
|
393
|
+
if (restoreLocation && restoreType && config && shouldStart) {
|
|
394
|
+
let backupPath = ''
|
|
395
|
+
|
|
396
|
+
if (restoreType === 'connection') {
|
|
397
|
+
// Create dump from remote database
|
|
398
|
+
const timestamp = Date.now()
|
|
399
|
+
tempDumpPath = join(tmpdir(), `spindb-dump-${timestamp}.dump`)
|
|
400
|
+
|
|
401
|
+
let dumpSuccess = false
|
|
402
|
+
let attempts = 0
|
|
403
|
+
const maxAttempts = 2 // Allow one retry after installing deps
|
|
404
|
+
|
|
405
|
+
while (!dumpSuccess && attempts < maxAttempts) {
|
|
406
|
+
attempts++
|
|
407
|
+
const dumpSpinner = createSpinner(
|
|
408
|
+
'Creating dump from remote database...',
|
|
409
|
+
)
|
|
410
|
+
dumpSpinner.start()
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
await dbEngine.dumpFromConnectionString(
|
|
414
|
+
restoreLocation,
|
|
415
|
+
tempDumpPath,
|
|
282
416
|
)
|
|
283
|
-
dumpSpinner.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
e.message.includes('pg_dump not found') ||
|
|
300
|
-
e.message.includes('ENOENT')
|
|
301
|
-
) {
|
|
302
|
-
const installed = await promptInstallDependencies('pg_dump')
|
|
303
|
-
if (!installed) {
|
|
304
|
-
process.exit(1)
|
|
305
|
-
}
|
|
306
|
-
// Loop will retry
|
|
307
|
-
continue
|
|
417
|
+
dumpSpinner.succeed('Dump created from remote database')
|
|
418
|
+
backupPath = tempDumpPath
|
|
419
|
+
dumpSuccess = true
|
|
420
|
+
} catch (err) {
|
|
421
|
+
const e = err as Error
|
|
422
|
+
dumpSpinner.fail('Failed to create dump')
|
|
423
|
+
|
|
424
|
+
// Check if this is a missing tool error
|
|
425
|
+
if (
|
|
426
|
+
e.message.includes('pg_dump not found') ||
|
|
427
|
+
e.message.includes('ENOENT')
|
|
428
|
+
) {
|
|
429
|
+
const installed = await promptInstallDependencies('pg_dump')
|
|
430
|
+
if (!installed) {
|
|
431
|
+
process.exit(1)
|
|
308
432
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.error(error('pg_dump error:'))
|
|
312
|
-
console.log(chalk.gray(` ${e.message}`))
|
|
313
|
-
process.exit(1)
|
|
433
|
+
// Loop will retry
|
|
434
|
+
continue
|
|
314
435
|
}
|
|
315
|
-
}
|
|
316
436
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
console.
|
|
437
|
+
console.log()
|
|
438
|
+
console.error(error('pg_dump error:'))
|
|
439
|
+
console.log(chalk.gray(` ${e.message}`))
|
|
320
440
|
process.exit(1)
|
|
321
441
|
}
|
|
322
|
-
} else {
|
|
323
|
-
backupPath = restoreLocation
|
|
324
442
|
}
|
|
325
443
|
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
444
|
+
// Safety check - should never reach here without backupPath set
|
|
445
|
+
if (!dumpSuccess) {
|
|
446
|
+
console.error(error('Failed to create dump after retries'))
|
|
447
|
+
process.exit(1)
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
backupPath = restoreLocation
|
|
451
|
+
}
|
|
329
452
|
|
|
330
|
-
|
|
331
|
-
|
|
453
|
+
// Detect backup format
|
|
454
|
+
const detectSpinner = createSpinner('Detecting backup format...')
|
|
455
|
+
detectSpinner.start()
|
|
332
456
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
restoreSpinner.start()
|
|
457
|
+
const format = await dbEngine.detectBackupFormat(backupPath)
|
|
458
|
+
detectSpinner.succeed(`Detected: ${format.description}`)
|
|
336
459
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
})
|
|
460
|
+
// Restore backup
|
|
461
|
+
const restoreSpinner = createSpinner('Restoring backup...')
|
|
462
|
+
restoreSpinner.start()
|
|
341
463
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
464
|
+
const result = await dbEngine.restore(config, backupPath, {
|
|
465
|
+
database,
|
|
466
|
+
createDatabase: false, // Already created above
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
if (result.code === 0 || !result.stderr) {
|
|
470
|
+
restoreSpinner.succeed('Backup restored successfully')
|
|
471
|
+
} else {
|
|
472
|
+
restoreSpinner.warn('Restore completed with warnings')
|
|
473
|
+
if (result.stderr) {
|
|
474
|
+
console.log(chalk.yellow('\n Warnings:'))
|
|
475
|
+
const lines = result.stderr.split('\n').slice(0, 5)
|
|
476
|
+
lines.forEach((line) => {
|
|
477
|
+
if (line.trim()) {
|
|
478
|
+
console.log(chalk.gray(` ${line}`))
|
|
356
479
|
}
|
|
480
|
+
})
|
|
481
|
+
if (result.stderr.split('\n').length > 5) {
|
|
482
|
+
console.log(chalk.gray(' ...'))
|
|
357
483
|
}
|
|
358
484
|
}
|
|
359
485
|
}
|
|
360
486
|
}
|
|
361
487
|
|
|
488
|
+
// Commit the transaction - all operations succeeded
|
|
489
|
+
tx.commit()
|
|
490
|
+
|
|
362
491
|
// Show success message
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
const connectionString = dbEngine.getConnectionString(
|
|
492
|
+
const finalConfig = await containerManager.getConfig(containerName)
|
|
493
|
+
if (finalConfig) {
|
|
494
|
+
const connectionString = dbEngine.getConnectionString(finalConfig)
|
|
366
495
|
|
|
367
496
|
console.log()
|
|
368
|
-
console.log(
|
|
497
|
+
console.log(
|
|
498
|
+
connectionBox(containerName, connectionString, finalConfig.port),
|
|
499
|
+
)
|
|
369
500
|
console.log()
|
|
370
|
-
console.log(chalk.gray(' Connect with:'))
|
|
371
|
-
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
372
501
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
|
|
377
|
-
const args =
|
|
378
|
-
platform() === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
379
|
-
|
|
380
|
-
await new Promise<void>((resolve, reject) => {
|
|
381
|
-
const proc = spawn(cmd, args, {
|
|
382
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
383
|
-
})
|
|
384
|
-
proc.stdin?.write(connectionString)
|
|
385
|
-
proc.stdin?.end()
|
|
386
|
-
proc.on('close', (code) => {
|
|
387
|
-
if (code === 0) resolve()
|
|
388
|
-
else
|
|
389
|
-
reject(
|
|
390
|
-
new Error(`Clipboard command exited with code ${code}`),
|
|
391
|
-
)
|
|
392
|
-
})
|
|
393
|
-
proc.on('error', reject)
|
|
394
|
-
})
|
|
502
|
+
if (shouldStart) {
|
|
503
|
+
console.log(chalk.gray(' Connect with:'))
|
|
504
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
395
505
|
|
|
506
|
+
// Copy connection string to clipboard
|
|
507
|
+
const copied =
|
|
508
|
+
await platformService.copyToClipboard(connectionString)
|
|
509
|
+
if (copied) {
|
|
396
510
|
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
397
|
-
} catch {
|
|
398
|
-
// Ignore clipboard errors
|
|
399
511
|
}
|
|
512
|
+
} else {
|
|
513
|
+
console.log(chalk.gray(' Start the container:'))
|
|
514
|
+
console.log(chalk.cyan(` spindb start ${containerName}`))
|
|
400
515
|
}
|
|
401
516
|
|
|
402
517
|
console.log()
|
|
@@ -404,23 +519,28 @@ export const createCommand = new Command('create')
|
|
|
404
519
|
} catch (err) {
|
|
405
520
|
const e = err as Error
|
|
406
521
|
|
|
407
|
-
// Check if this is a missing tool error
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
522
|
+
// Check if this is a missing tool error (PostgreSQL or MySQL)
|
|
523
|
+
const missingToolPatterns = [
|
|
524
|
+
// PostgreSQL
|
|
525
|
+
'pg_restore not found',
|
|
526
|
+
'psql not found',
|
|
527
|
+
'pg_dump not found',
|
|
528
|
+
// MySQL
|
|
529
|
+
'mysql not found',
|
|
530
|
+
'mysqldump not found',
|
|
531
|
+
'mysqld not found',
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
const matchingPattern = missingToolPatterns.find((p) =>
|
|
535
|
+
e.message.includes(p),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if (matchingPattern) {
|
|
539
|
+
const missingTool = matchingPattern.replace(' not found', '')
|
|
418
540
|
const installed = await promptInstallDependencies(missingTool)
|
|
419
541
|
if (installed) {
|
|
420
542
|
console.log(
|
|
421
|
-
chalk.yellow(
|
|
422
|
-
' Please re-run your command to continue.',
|
|
423
|
-
),
|
|
543
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
424
544
|
)
|
|
425
545
|
}
|
|
426
546
|
process.exit(1)
|
package/cli/commands/delete.ts
CHANGED
|
@@ -57,7 +57,9 @@ export const deleteCommand = new Command('delete')
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Check if running
|
|
60
|
-
const running = await processManager.isRunning(containerName
|
|
60
|
+
const running = await processManager.isRunning(containerName, {
|
|
61
|
+
engine: config.engine,
|
|
62
|
+
})
|
|
61
63
|
if (running) {
|
|
62
64
|
if (options.force) {
|
|
63
65
|
// Stop the container first
|