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/cli/commands/list.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { containerManager } from '../../core/container-manager'
|
|
4
4
|
import { getEngine } from '../../engines'
|
|
5
|
-
import {
|
|
5
|
+
import { uiInfo, uiError, formatBytes } from '../ui/theme'
|
|
6
6
|
import { getEngineIcon } from '../constants'
|
|
7
7
|
import { Engine } from '../../types'
|
|
8
8
|
import { basename } from 'path'
|
|
@@ -12,8 +12,8 @@ import type { ContainerConfig } from '../../types'
|
|
|
12
12
|
* Pad string to width, accounting for emoji taking 2 display columns
|
|
13
13
|
*/
|
|
14
14
|
function padWithEmoji(str: string, width: number): string {
|
|
15
|
-
// Count emojis
|
|
16
|
-
const emojiCount = (str.match(
|
|
15
|
+
// Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
|
|
16
|
+
const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
|
|
17
17
|
return str.padEnd(width + emojiCount)
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -62,7 +62,9 @@ export const listCommand = new Command('list')
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (containers.length === 0) {
|
|
65
|
-
console.log(
|
|
65
|
+
console.log(
|
|
66
|
+
uiInfo('No containers found. Create one with: spindb create'),
|
|
67
|
+
)
|
|
66
68
|
return
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -107,8 +109,9 @@ export const listCommand = new Command('list')
|
|
|
107
109
|
let portOrPath: string
|
|
108
110
|
if (container.engine === Engine.SQLite) {
|
|
109
111
|
const fileName = basename(container.database)
|
|
110
|
-
// Truncate if longer than
|
|
111
|
-
portOrPath =
|
|
112
|
+
// Truncate if longer than 8 chars to fit in 8-char column
|
|
113
|
+
portOrPath =
|
|
114
|
+
fileName.length > 8 ? fileName.slice(0, 7) + '…' : fileName
|
|
112
115
|
} else {
|
|
113
116
|
portOrPath = String(container.port)
|
|
114
117
|
}
|
|
@@ -126,31 +129,43 @@ export const listCommand = new Command('list')
|
|
|
126
129
|
|
|
127
130
|
console.log()
|
|
128
131
|
|
|
129
|
-
const serverContainers = containers.filter(
|
|
130
|
-
|
|
132
|
+
const serverContainers = containers.filter(
|
|
133
|
+
(c) => c.engine !== Engine.SQLite,
|
|
134
|
+
)
|
|
135
|
+
const sqliteContainers = containers.filter(
|
|
136
|
+
(c) => c.engine === Engine.SQLite,
|
|
137
|
+
)
|
|
131
138
|
|
|
132
|
-
const running = serverContainers.filter(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
139
|
+
const running = serverContainers.filter(
|
|
140
|
+
(c) => c.status === 'running',
|
|
141
|
+
).length
|
|
142
|
+
const stopped = serverContainers.filter(
|
|
143
|
+
(c) => c.status !== 'running',
|
|
144
|
+
).length
|
|
145
|
+
const available = sqliteContainers.filter(
|
|
146
|
+
(c) => c.status === 'running',
|
|
147
|
+
).length
|
|
148
|
+
const missing = sqliteContainers.filter(
|
|
149
|
+
(c) => c.status !== 'running',
|
|
150
|
+
).length
|
|
136
151
|
|
|
137
152
|
const parts: string[] = []
|
|
138
153
|
if (serverContainers.length > 0) {
|
|
139
154
|
parts.push(`${running} running, ${stopped} stopped`)
|
|
140
155
|
}
|
|
141
156
|
if (sqliteContainers.length > 0) {
|
|
142
|
-
parts.push(
|
|
157
|
+
parts.push(
|
|
158
|
+
`${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`,
|
|
159
|
+
)
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
console.log(
|
|
146
|
-
chalk.gray(
|
|
147
|
-
` ${containers.length} container(s): ${parts.join('; ')}`,
|
|
148
|
-
),
|
|
163
|
+
chalk.gray(` ${containers.length} container(s): ${parts.join('; ')}`),
|
|
149
164
|
)
|
|
150
165
|
console.log()
|
|
151
|
-
} catch (
|
|
152
|
-
const e =
|
|
153
|
-
console.error(
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const e = error as Error
|
|
168
|
+
console.error(uiError(e.message))
|
|
154
169
|
process.exit(1)
|
|
155
170
|
}
|
|
156
171
|
})
|
package/cli/commands/logs.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { readFile } from 'fs/promises'
|
|
|
5
5
|
import { containerManager } from '../../core/container-manager'
|
|
6
6
|
import { paths } from '../../config/paths'
|
|
7
7
|
import { promptContainerSelect } from '../ui/prompts'
|
|
8
|
-
import {
|
|
8
|
+
import { uiError, uiWarning, uiInfo } from '../ui/theme'
|
|
9
9
|
|
|
10
10
|
function getLastNLines(content: string, n: number): string {
|
|
11
11
|
const lines = content.split('\n')
|
|
@@ -32,7 +32,7 @@ export const logsCommand = new Command('logs')
|
|
|
32
32
|
const containers = await containerManager.list()
|
|
33
33
|
|
|
34
34
|
if (containers.length === 0) {
|
|
35
|
-
console.log(
|
|
35
|
+
console.log(uiWarning('No containers found'))
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ export const logsCommand = new Command('logs')
|
|
|
46
46
|
|
|
47
47
|
const config = await containerManager.getConfig(containerName)
|
|
48
48
|
if (!config) {
|
|
49
|
-
console.error(
|
|
49
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
50
50
|
process.exit(1)
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ export const logsCommand = new Command('logs')
|
|
|
56
56
|
|
|
57
57
|
if (!existsSync(logPath)) {
|
|
58
58
|
console.log(
|
|
59
|
-
|
|
59
|
+
uiInfo(
|
|
60
60
|
`No log file found for "${containerName}". The container may not have been started yet.`,
|
|
61
61
|
),
|
|
62
62
|
)
|
|
@@ -84,17 +84,27 @@ export const logsCommand = new Command('logs')
|
|
|
84
84
|
|
|
85
85
|
if (options.follow) {
|
|
86
86
|
const lineCount = parseInt(options.lines || '50', 10)
|
|
87
|
-
const child = spawn(
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const child = spawn(
|
|
88
|
+
'tail',
|
|
89
|
+
['-n', String(lineCount), '-f', logPath],
|
|
90
|
+
{
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
},
|
|
93
|
+
)
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
// Use named handler so we can remove it to prevent listener leaks
|
|
96
|
+
const sigintHandler = () => {
|
|
97
|
+
process.removeListener('SIGINT', sigintHandler)
|
|
92
98
|
child.kill('SIGTERM')
|
|
93
99
|
process.exit(0)
|
|
94
|
-
}
|
|
100
|
+
}
|
|
101
|
+
process.on('SIGINT', sigintHandler)
|
|
95
102
|
|
|
96
103
|
await new Promise<void>((resolve) => {
|
|
97
|
-
child.on('close', () =>
|
|
104
|
+
child.on('close', () => {
|
|
105
|
+
process.removeListener('SIGINT', sigintHandler)
|
|
106
|
+
resolve()
|
|
107
|
+
})
|
|
98
108
|
})
|
|
99
109
|
return
|
|
100
110
|
}
|
|
@@ -103,15 +113,15 @@ export const logsCommand = new Command('logs')
|
|
|
103
113
|
const content = await readFile(logPath, 'utf-8')
|
|
104
114
|
|
|
105
115
|
if (content.trim() === '') {
|
|
106
|
-
console.log(
|
|
116
|
+
console.log(uiInfo('Log file is empty'))
|
|
107
117
|
return
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
const output = getLastNLines(content, lineCount)
|
|
111
121
|
console.log(output)
|
|
112
|
-
} catch (
|
|
113
|
-
const e =
|
|
114
|
-
console.error(
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const e = error as Error
|
|
124
|
+
console.error(uiError(e.message))
|
|
115
125
|
process.exit(1)
|
|
116
126
|
}
|
|
117
127
|
},
|
|
@@ -25,14 +25,15 @@ import {
|
|
|
25
25
|
import { createSpinner } from '../../ui/spinner'
|
|
26
26
|
import {
|
|
27
27
|
header,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
uiSuccess,
|
|
29
|
+
uiError,
|
|
30
|
+
uiWarning,
|
|
31
31
|
connectionBox,
|
|
32
32
|
formatBytes,
|
|
33
33
|
} from '../../ui/theme'
|
|
34
34
|
import { getEngineIcon } from '../../constants'
|
|
35
35
|
import { type Engine } from '../../../types'
|
|
36
|
+
import { pressEnterToContinue } from './shared'
|
|
36
37
|
|
|
37
38
|
function generateBackupTimestamp(): string {
|
|
38
39
|
const now = new Date()
|
|
@@ -65,7 +66,7 @@ export async function handleCreateForRestore(): Promise<{
|
|
|
65
66
|
const portAvailable = await portManager.isPortAvailable(port)
|
|
66
67
|
if (!portAvailable) {
|
|
67
68
|
console.log(
|
|
68
|
-
|
|
69
|
+
uiError(`Port ${port} is in use. Please choose a different port.`),
|
|
69
70
|
)
|
|
70
71
|
return null
|
|
71
72
|
}
|
|
@@ -77,13 +78,17 @@ export async function handleCreateForRestore(): Promise<{
|
|
|
77
78
|
|
|
78
79
|
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
79
80
|
if (isInstalled) {
|
|
80
|
-
binarySpinner.succeed(
|
|
81
|
+
binarySpinner.succeed(
|
|
82
|
+
`${dbEngine.displayName} ${version} binaries ready (cached)`,
|
|
83
|
+
)
|
|
81
84
|
} else {
|
|
82
85
|
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
83
86
|
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
84
87
|
binarySpinner.text = message
|
|
85
88
|
})
|
|
86
|
-
binarySpinner.succeed(
|
|
89
|
+
binarySpinner.succeed(
|
|
90
|
+
`${dbEngine.displayName} ${version} binaries downloaded`,
|
|
91
|
+
)
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
while (await containerManager.exists(containerName)) {
|
|
@@ -136,7 +141,7 @@ export async function handleCreateForRestore(): Promise<{
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
console.log()
|
|
139
|
-
console.log(
|
|
144
|
+
console.log(uiSuccess('Container ready for restore'))
|
|
140
145
|
console.log()
|
|
141
146
|
|
|
142
147
|
return { name: containerName, config }
|
|
@@ -184,7 +189,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
184
189
|
containerName = selectedContainer
|
|
185
190
|
config = await containerManager.getConfig(containerName)
|
|
186
191
|
if (!config) {
|
|
187
|
-
console.error(
|
|
192
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
188
193
|
return
|
|
189
194
|
}
|
|
190
195
|
}
|
|
@@ -210,7 +215,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
210
215
|
missingDeps = await getMissingDependencies(config.engine)
|
|
211
216
|
if (missingDeps.length > 0) {
|
|
212
217
|
console.log(
|
|
213
|
-
|
|
218
|
+
uiError(
|
|
214
219
|
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
215
220
|
),
|
|
216
221
|
)
|
|
@@ -301,8 +306,8 @@ export async function handleRestore(): Promise<void> {
|
|
|
301
306
|
backupPath = tempDumpPath
|
|
302
307
|
isTempFile = true
|
|
303
308
|
dumpSuccess = true
|
|
304
|
-
} catch (
|
|
305
|
-
const e =
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const e = error as Error
|
|
306
311
|
dumpSpinner.fail('Failed to create dump')
|
|
307
312
|
|
|
308
313
|
if (
|
|
@@ -313,15 +318,19 @@ export async function handleRestore(): Promise<void> {
|
|
|
313
318
|
const missingTool = e.message.includes('mysqldump')
|
|
314
319
|
? 'mysqldump'
|
|
315
320
|
: 'pg_dump'
|
|
316
|
-
const toolEngine =
|
|
317
|
-
|
|
321
|
+
const toolEngine =
|
|
322
|
+
missingTool === 'mysqldump' ? 'mysql' : 'postgresql'
|
|
323
|
+
const installed = await promptInstallDependencies(
|
|
324
|
+
missingTool,
|
|
325
|
+
toolEngine as Engine,
|
|
326
|
+
)
|
|
318
327
|
if (installed) {
|
|
319
328
|
continue
|
|
320
329
|
}
|
|
321
330
|
} else {
|
|
322
331
|
const dumpTool = config.engine === 'mysql' ? 'mysqldump' : 'pg_dump'
|
|
323
332
|
console.log()
|
|
324
|
-
console.log(
|
|
333
|
+
console.log(uiError(`${dumpTool} error:`))
|
|
325
334
|
console.log(chalk.gray(` ${e.message}`))
|
|
326
335
|
console.log()
|
|
327
336
|
}
|
|
@@ -344,7 +353,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
344
353
|
}
|
|
345
354
|
|
|
346
355
|
if (!dumpSuccess) {
|
|
347
|
-
console.log(
|
|
356
|
+
console.log(uiError('Failed to create dump after retries'))
|
|
348
357
|
return
|
|
349
358
|
}
|
|
350
359
|
} else {
|
|
@@ -403,7 +412,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
403
412
|
createDatabase: false,
|
|
404
413
|
})
|
|
405
414
|
|
|
406
|
-
if (result.code === 0
|
|
415
|
+
if (result.code === 0) {
|
|
407
416
|
restoreSpinner.succeed('Backup restored successfully')
|
|
408
417
|
} else {
|
|
409
418
|
const stderr = result.stderr || ''
|
|
@@ -415,9 +424,9 @@ export async function handleRestore(): Promise<void> {
|
|
|
415
424
|
) {
|
|
416
425
|
restoreSpinner.fail('Version compatibility detected')
|
|
417
426
|
console.log()
|
|
418
|
-
console.log(
|
|
427
|
+
console.log(uiError('PostgreSQL version incompatibility detected:'))
|
|
419
428
|
console.log(
|
|
420
|
-
|
|
429
|
+
uiWarning('Your pg_restore version is too old for this backup file.'),
|
|
421
430
|
)
|
|
422
431
|
|
|
423
432
|
console.log(chalk.yellow('Cleaning up failed database...'))
|
|
@@ -467,23 +476,20 @@ export async function handleRestore(): Promise<void> {
|
|
|
467
476
|
upgradeSpinner.succeed('PostgreSQL client tools upgraded')
|
|
468
477
|
console.log()
|
|
469
478
|
console.log(
|
|
470
|
-
|
|
479
|
+
uiSuccess('Please try the restore again with the updated tools.'),
|
|
471
480
|
)
|
|
472
|
-
await
|
|
473
|
-
console.log(chalk.gray('Press Enter to continue...'))
|
|
474
|
-
process.stdin.once('data', resolve)
|
|
475
|
-
})
|
|
481
|
+
await pressEnterToContinue()
|
|
476
482
|
return
|
|
477
483
|
} else {
|
|
478
484
|
upgradeSpinner.fail('Upgrade failed')
|
|
479
485
|
console.log()
|
|
480
486
|
console.log(
|
|
481
|
-
|
|
487
|
+
uiError('Automatic upgrade failed. Please upgrade manually:'),
|
|
482
488
|
)
|
|
483
489
|
const pgPackage = getPostgresHomebrewPackage()
|
|
484
490
|
const latestMajor = pgPackage.split('@')[1]
|
|
485
491
|
console.log(
|
|
486
|
-
|
|
492
|
+
uiWarning(
|
|
487
493
|
` macOS: brew install ${pgPackage} && brew link --force ${pgPackage}`,
|
|
488
494
|
),
|
|
489
495
|
)
|
|
@@ -493,7 +499,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
493
499
|
),
|
|
494
500
|
)
|
|
495
501
|
console.log(
|
|
496
|
-
|
|
502
|
+
uiWarning(
|
|
497
503
|
` Ubuntu/Debian: sudo apt update && sudo apt install postgresql-client-${latestMajor}`,
|
|
498
504
|
),
|
|
499
505
|
)
|
|
@@ -502,15 +508,12 @@ export async function handleRestore(): Promise<void> {
|
|
|
502
508
|
` This installs PostgreSQL ${latestMajor} client tools: pg_restore, pg_dump, psql, and libpq`,
|
|
503
509
|
),
|
|
504
510
|
)
|
|
505
|
-
await
|
|
506
|
-
console.log(chalk.gray('Press Enter to continue...'))
|
|
507
|
-
process.stdin.once('data', resolve)
|
|
508
|
-
})
|
|
511
|
+
await pressEnterToContinue()
|
|
509
512
|
return
|
|
510
513
|
}
|
|
511
514
|
} catch {
|
|
512
515
|
upgradeSpinner.fail('Upgrade failed')
|
|
513
|
-
console.log(
|
|
516
|
+
console.log(uiError('Failed to upgrade PostgreSQL client tools'))
|
|
514
517
|
console.log(
|
|
515
518
|
chalk.gray(
|
|
516
519
|
'Manual upgrade may be required for pg_restore, pg_dump, and psql',
|
|
@@ -525,7 +528,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
525
528
|
} else {
|
|
526
529
|
console.log()
|
|
527
530
|
console.log(
|
|
528
|
-
|
|
531
|
+
uiWarning(
|
|
529
532
|
'Restore cancelled. Please upgrade PostgreSQL client tools manually and try again.',
|
|
530
533
|
),
|
|
531
534
|
)
|
|
@@ -552,10 +555,10 @@ export async function handleRestore(): Promise<void> {
|
|
|
552
555
|
}
|
|
553
556
|
}
|
|
554
557
|
|
|
555
|
-
if (result.code === 0
|
|
558
|
+
if (result.code === 0) {
|
|
556
559
|
const connectionString = engine.getConnectionString(config, databaseName)
|
|
557
560
|
console.log()
|
|
558
|
-
console.log(
|
|
561
|
+
console.log(uiSuccess(`Database "${databaseName}" restored`))
|
|
559
562
|
console.log(chalk.gray(' Connection string:'))
|
|
560
563
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
561
564
|
|
|
@@ -591,7 +594,7 @@ export async function handleBackup(): Promise<void> {
|
|
|
591
594
|
const running = containers.filter((c) => c.status === 'running')
|
|
592
595
|
|
|
593
596
|
if (running.length === 0) {
|
|
594
|
-
console.log(
|
|
597
|
+
console.log(uiWarning('No running containers. Start a container first.'))
|
|
595
598
|
await inquirer.prompt([
|
|
596
599
|
{
|
|
597
600
|
type: 'input',
|
|
@@ -611,7 +614,7 @@ export async function handleBackup(): Promise<void> {
|
|
|
611
614
|
|
|
612
615
|
const config = await containerManager.getConfig(containerName)
|
|
613
616
|
if (!config) {
|
|
614
|
-
console.log(
|
|
617
|
+
console.log(uiError(`Container "${containerName}" not found`))
|
|
615
618
|
return
|
|
616
619
|
}
|
|
617
620
|
|
|
@@ -638,7 +641,7 @@ export async function handleBackup(): Promise<void> {
|
|
|
638
641
|
missingDeps = await getMissingDependencies(config.engine)
|
|
639
642
|
if (missingDeps.length > 0) {
|
|
640
643
|
console.log(
|
|
641
|
-
|
|
644
|
+
uiError(
|
|
642
645
|
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
643
646
|
),
|
|
644
647
|
)
|
|
@@ -685,17 +688,17 @@ export async function handleBackup(): Promise<void> {
|
|
|
685
688
|
backupSpinner.succeed('Backup created successfully')
|
|
686
689
|
|
|
687
690
|
console.log()
|
|
688
|
-
console.log(
|
|
691
|
+
console.log(uiSuccess('Backup complete'))
|
|
689
692
|
console.log()
|
|
690
693
|
console.log(chalk.gray(' File:'), chalk.cyan(result.path))
|
|
691
694
|
console.log(chalk.gray(' Size:'), chalk.white(formatBytes(result.size)))
|
|
692
695
|
console.log(chalk.gray(' Format:'), chalk.white(result.format))
|
|
693
696
|
console.log()
|
|
694
|
-
} catch (
|
|
695
|
-
const e =
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const e = error as Error
|
|
696
699
|
backupSpinner.fail('Backup failed')
|
|
697
700
|
console.log()
|
|
698
|
-
console.log(
|
|
701
|
+
console.log(uiError(e.message))
|
|
699
702
|
console.log()
|
|
700
703
|
}
|
|
701
704
|
|
|
@@ -713,13 +716,13 @@ export async function handleClone(): Promise<void> {
|
|
|
713
716
|
const stopped = containers.filter((c) => c.status !== 'running')
|
|
714
717
|
|
|
715
718
|
if (containers.length === 0) {
|
|
716
|
-
console.log(
|
|
719
|
+
console.log(uiWarning('No containers found'))
|
|
717
720
|
return
|
|
718
721
|
}
|
|
719
722
|
|
|
720
723
|
if (stopped.length === 0) {
|
|
721
724
|
console.log(
|
|
722
|
-
|
|
725
|
+
uiWarning(
|
|
723
726
|
'All containers are running. Stop a container first to clone it.',
|
|
724
727
|
),
|
|
725
728
|
)
|
|
@@ -733,6 +736,12 @@ export async function handleClone(): Promise<void> {
|
|
|
733
736
|
)
|
|
734
737
|
if (!sourceName) return
|
|
735
738
|
|
|
739
|
+
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
740
|
+
if (!sourceConfig) {
|
|
741
|
+
console.log(uiError(`Container "${sourceName}" not found`))
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
|
|
736
745
|
const { targetName } = await inquirer.prompt<{ targetName: string }>([
|
|
737
746
|
{
|
|
738
747
|
type: 'input',
|
|
@@ -749,16 +758,30 @@ export async function handleClone(): Promise<void> {
|
|
|
749
758
|
},
|
|
750
759
|
])
|
|
751
760
|
|
|
761
|
+
// Check if target container already exists
|
|
762
|
+
if (
|
|
763
|
+
await containerManager.exists(targetName, { engine: sourceConfig.engine })
|
|
764
|
+
) {
|
|
765
|
+
console.log(uiError(`Container "${targetName}" already exists`))
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
752
769
|
const spinner = createSpinner(`Cloning ${sourceName} to ${targetName}...`)
|
|
753
770
|
spinner.start()
|
|
754
771
|
|
|
755
|
-
|
|
772
|
+
try {
|
|
773
|
+
const newConfig = await containerManager.clone(sourceName, targetName)
|
|
756
774
|
|
|
757
|
-
|
|
775
|
+
spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
|
|
758
776
|
|
|
759
|
-
|
|
760
|
-
|
|
777
|
+
const engine = getEngine(newConfig.engine)
|
|
778
|
+
const connectionString = engine.getConnectionString(newConfig)
|
|
761
779
|
|
|
762
|
-
|
|
763
|
-
|
|
780
|
+
console.log()
|
|
781
|
+
console.log(connectionBox(targetName, connectionString, newConfig.port))
|
|
782
|
+
} catch (error) {
|
|
783
|
+
const e = error as Error
|
|
784
|
+
spinner.fail(`Failed to clone "${sourceName}"`)
|
|
785
|
+
console.log(uiError(e.message))
|
|
786
|
+
}
|
|
764
787
|
}
|