spindb 0.8.2 → 0.9.1
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 +87 -7
- package/cli/commands/clone.ts +6 -0
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +170 -8
- package/cli/commands/doctor.ts +320 -0
- package/cli/commands/edit.ts +209 -9
- package/cli/commands/engines.ts +34 -3
- package/cli/commands/info.ts +81 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/logs.ts +9 -3
- package/cli/commands/menu/backup-handlers.ts +52 -21
- package/cli/commands/menu/container-handlers.ts +433 -127
- package/cli/commands/menu/engine-handlers.ts +128 -4
- package/cli/commands/menu/index.ts +5 -1
- package/cli/commands/menu/shell-handlers.ts +105 -21
- package/cli/commands/menu/sql-handlers.ts +16 -4
- package/cli/commands/menu/update-handlers.ts +278 -0
- package/cli/commands/restore.ts +83 -23
- package/cli/commands/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/helpers.ts +41 -1
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +148 -7
- package/config/engine-defaults.ts +14 -0
- package/config/os-dependencies.ts +66 -0
- package/config/paths.ts +8 -0
- package/core/container-manager.ts +191 -32
- package/core/dependency-manager.ts +18 -0
- package/core/error-handler.ts +31 -0
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- package/engines/index.ts +4 -0
- package/engines/mysql/backup.ts +53 -36
- package/engines/mysql/index.ts +48 -5
- package/engines/postgresql/index.ts +6 -0
- package/engines/sqlite/index.ts +606 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
package/cli/commands/info.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import { basename } from 'path'
|
|
4
6
|
import { containerManager } from '../../core/container-manager'
|
|
5
7
|
import { processManager } from '../../core/process-manager'
|
|
6
8
|
import { paths } from '../../config/paths'
|
|
7
9
|
import { getEngine } from '../../engines'
|
|
8
10
|
import { error, info, header } from '../ui/theme'
|
|
9
11
|
import { getEngineIcon } from '../constants'
|
|
10
|
-
import type
|
|
12
|
+
import { Engine, type ContainerConfig } from '../../types'
|
|
11
13
|
|
|
12
14
|
function formatDate(dateString: string): string {
|
|
13
15
|
const date = new Date(dateString)
|
|
@@ -16,7 +18,13 @@ function formatDate(dateString: string): string {
|
|
|
16
18
|
|
|
17
19
|
async function getActualStatus(
|
|
18
20
|
config: ContainerConfig,
|
|
19
|
-
): Promise<'running' | 'stopped'> {
|
|
21
|
+
): Promise<'running' | 'stopped' | 'available' | 'missing'> {
|
|
22
|
+
// SQLite: check file existence instead of running status
|
|
23
|
+
if (config.engine === Engine.SQLite) {
|
|
24
|
+
const fileExists = existsSync(config.database)
|
|
25
|
+
return fileExists ? 'available' : 'missing'
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
const running = await processManager.isRunning(config.name, {
|
|
21
29
|
engine: config.engine,
|
|
22
30
|
})
|
|
@@ -51,10 +59,21 @@ async function displayContainerInfo(
|
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
const icon = getEngineIcon(config.engine)
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
const isSQLite = config.engine === Engine.SQLite
|
|
63
|
+
|
|
64
|
+
// Status display based on engine type
|
|
65
|
+
let statusDisplay: string
|
|
66
|
+
if (isSQLite) {
|
|
67
|
+
statusDisplay =
|
|
68
|
+
actualStatus === 'available'
|
|
69
|
+
? chalk.blue('🔵 available')
|
|
70
|
+
: chalk.gray('⚪ missing')
|
|
71
|
+
} else {
|
|
72
|
+
statusDisplay =
|
|
73
|
+
actualStatus === 'running'
|
|
74
|
+
? chalk.green('● running')
|
|
75
|
+
: chalk.gray('○ stopped')
|
|
76
|
+
}
|
|
58
77
|
|
|
59
78
|
console.log()
|
|
60
79
|
console.log(header(`Container: ${config.name}`))
|
|
@@ -67,26 +86,41 @@ async function displayContainerInfo(
|
|
|
67
86
|
console.log(
|
|
68
87
|
chalk.gray(' ') + chalk.white('Status:'.padEnd(14)) + statusDisplay,
|
|
69
88
|
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
|
|
90
|
+
// Show file path for SQLite, port for server databases
|
|
91
|
+
if (isSQLite) {
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.gray(' ') +
|
|
94
|
+
chalk.white('File:'.padEnd(14)) +
|
|
95
|
+
chalk.green(config.database),
|
|
96
|
+
)
|
|
97
|
+
} else {
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.gray(' ') +
|
|
100
|
+
chalk.white('Port:'.padEnd(14)) +
|
|
101
|
+
chalk.green(String(config.port)),
|
|
102
|
+
)
|
|
103
|
+
console.log(
|
|
104
|
+
chalk.gray(' ') +
|
|
105
|
+
chalk.white('Database:'.padEnd(14)) +
|
|
106
|
+
chalk.yellow(config.database),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
80
110
|
console.log(
|
|
81
111
|
chalk.gray(' ') +
|
|
82
112
|
chalk.white('Created:'.padEnd(14)) +
|
|
83
113
|
chalk.gray(formatDate(config.created)),
|
|
84
114
|
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
115
|
+
|
|
116
|
+
// Don't show data dir for SQLite (file path is already shown)
|
|
117
|
+
if (!isSQLite) {
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.gray(' ') +
|
|
120
|
+
chalk.white('Data Dir:'.padEnd(14)) +
|
|
121
|
+
chalk.gray(dataDir),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
90
124
|
if (config.clonedFrom) {
|
|
91
125
|
console.log(
|
|
92
126
|
chalk.gray(' ') +
|
|
@@ -142,20 +176,41 @@ async function displayAllContainersInfo(
|
|
|
142
176
|
|
|
143
177
|
for (const container of containers) {
|
|
144
178
|
const actualStatus = await getActualStatus(container)
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
const isSQLite = container.engine === Engine.SQLite
|
|
180
|
+
|
|
181
|
+
// Status display based on engine type
|
|
182
|
+
let statusDisplay: string
|
|
183
|
+
if (isSQLite) {
|
|
184
|
+
statusDisplay =
|
|
185
|
+
actualStatus === 'available'
|
|
186
|
+
? chalk.blue('🔵 available')
|
|
187
|
+
: chalk.gray('⚪ missing')
|
|
188
|
+
} else {
|
|
189
|
+
statusDisplay =
|
|
190
|
+
actualStatus === 'running'
|
|
191
|
+
? chalk.green('● running')
|
|
192
|
+
: chalk.gray('○ stopped')
|
|
193
|
+
}
|
|
149
194
|
|
|
150
195
|
const icon = getEngineIcon(container.engine)
|
|
151
196
|
const engineDisplay = `${icon} ${container.engine}`
|
|
152
197
|
|
|
198
|
+
// Show truncated file path for SQLite instead of port
|
|
199
|
+
let portOrPath: string
|
|
200
|
+
if (isSQLite) {
|
|
201
|
+
const fileName = basename(container.database)
|
|
202
|
+
// Truncate if longer than 8 chars to fit in 8-char column
|
|
203
|
+
portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + '…' : fileName
|
|
204
|
+
} else {
|
|
205
|
+
portOrPath = String(container.port)
|
|
206
|
+
}
|
|
207
|
+
|
|
153
208
|
console.log(
|
|
154
209
|
chalk.gray(' ') +
|
|
155
210
|
chalk.cyan(container.name.padEnd(18)) +
|
|
156
211
|
chalk.white(engineDisplay.padEnd(13)) +
|
|
157
212
|
chalk.yellow(container.version.padEnd(10)) +
|
|
158
|
-
chalk.green(
|
|
213
|
+
chalk.green(portOrPath.padEnd(8)) +
|
|
159
214
|
chalk.gray(container.database.padEnd(16)) +
|
|
160
215
|
statusDisplay,
|
|
161
216
|
)
|
package/cli/commands/list.ts
CHANGED
|
@@ -4,11 +4,33 @@ import { containerManager } from '../../core/container-manager'
|
|
|
4
4
|
import { getEngine } from '../../engines'
|
|
5
5
|
import { info, error, formatBytes } from '../ui/theme'
|
|
6
6
|
import { getEngineIcon } from '../constants'
|
|
7
|
+
import { Engine } from '../../types'
|
|
8
|
+
import { basename } from 'path'
|
|
7
9
|
import type { ContainerConfig } from '../../types'
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Pad string to width, accounting for emoji taking 2 display columns
|
|
13
|
+
*/
|
|
14
|
+
function padWithEmoji(str: string, width: number): string {
|
|
15
|
+
// Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
|
|
16
|
+
const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
|
|
17
|
+
return str.padEnd(width + emojiCount)
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
async function getContainerSize(
|
|
10
21
|
container: ContainerConfig,
|
|
11
22
|
): Promise<number | null> {
|
|
23
|
+
// SQLite can always get size (it's just file size)
|
|
24
|
+
if (container.engine === Engine.SQLite) {
|
|
25
|
+
try {
|
|
26
|
+
const engine = getEngine(container.engine)
|
|
27
|
+
return await engine.getDatabaseSize(container)
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Server databases need to be running
|
|
12
34
|
if (container.status !== 'running') {
|
|
13
35
|
return null
|
|
14
36
|
}
|
|
@@ -62,22 +84,41 @@ export const listCommand = new Command('list')
|
|
|
62
84
|
const container = containers[i]
|
|
63
85
|
const size = sizes[i]
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
// SQLite uses different status labels (blue/white icons)
|
|
88
|
+
let statusDisplay: string
|
|
89
|
+
if (container.engine === Engine.SQLite) {
|
|
90
|
+
statusDisplay =
|
|
91
|
+
container.status === 'running'
|
|
92
|
+
? chalk.blue('🔵 available')
|
|
93
|
+
: chalk.gray('⚪ missing')
|
|
94
|
+
} else {
|
|
95
|
+
statusDisplay =
|
|
96
|
+
container.status === 'running'
|
|
97
|
+
? chalk.green('● running')
|
|
98
|
+
: chalk.gray('○ stopped')
|
|
99
|
+
}
|
|
69
100
|
|
|
70
101
|
const engineIcon = getEngineIcon(container.engine)
|
|
71
102
|
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
72
103
|
|
|
73
104
|
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
74
105
|
|
|
106
|
+
// SQLite shows truncated file name instead of port
|
|
107
|
+
let portOrPath: string
|
|
108
|
+
if (container.engine === Engine.SQLite) {
|
|
109
|
+
const fileName = basename(container.database)
|
|
110
|
+
// Truncate if longer than 8 chars to fit in 8-char column
|
|
111
|
+
portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + '…' : fileName
|
|
112
|
+
} else {
|
|
113
|
+
portOrPath = String(container.port)
|
|
114
|
+
}
|
|
115
|
+
|
|
75
116
|
console.log(
|
|
76
117
|
chalk.gray(' ') +
|
|
77
118
|
chalk.cyan(container.name.padEnd(20)) +
|
|
78
|
-
chalk.white(engineDisplay
|
|
119
|
+
chalk.white(padWithEmoji(engineDisplay, 14)) +
|
|
79
120
|
chalk.yellow(container.version.padEnd(10)) +
|
|
80
|
-
chalk.green(
|
|
121
|
+
chalk.green(portOrPath.padEnd(8)) +
|
|
81
122
|
chalk.magenta(sizeDisplay.padEnd(10)) +
|
|
82
123
|
statusDisplay,
|
|
83
124
|
)
|
|
@@ -85,11 +126,25 @@ export const listCommand = new Command('list')
|
|
|
85
126
|
|
|
86
127
|
console.log()
|
|
87
128
|
|
|
88
|
-
const
|
|
89
|
-
const
|
|
129
|
+
const serverContainers = containers.filter((c) => c.engine !== Engine.SQLite)
|
|
130
|
+
const sqliteContainers = containers.filter((c) => c.engine === Engine.SQLite)
|
|
131
|
+
|
|
132
|
+
const running = serverContainers.filter((c) => c.status === 'running').length
|
|
133
|
+
const stopped = serverContainers.filter((c) => c.status !== 'running').length
|
|
134
|
+
const available = sqliteContainers.filter((c) => c.status === 'running').length
|
|
135
|
+
const missing = sqliteContainers.filter((c) => c.status !== 'running').length
|
|
136
|
+
|
|
137
|
+
const parts: string[] = []
|
|
138
|
+
if (serverContainers.length > 0) {
|
|
139
|
+
parts.push(`${running} running, ${stopped} stopped`)
|
|
140
|
+
}
|
|
141
|
+
if (sqliteContainers.length > 0) {
|
|
142
|
+
parts.push(`${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
90
145
|
console.log(
|
|
91
146
|
chalk.gray(
|
|
92
|
-
` ${containers.length} container(s): ${
|
|
147
|
+
` ${containers.length} container(s): ${parts.join('; ')}`,
|
|
93
148
|
),
|
|
94
149
|
)
|
|
95
150
|
console.log()
|
package/cli/commands/logs.ts
CHANGED
|
@@ -88,13 +88,19 @@ export const logsCommand = new Command('logs')
|
|
|
88
88
|
stdio: 'inherit',
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
// Use named handler so we can remove it to prevent listener leaks
|
|
92
|
+
const sigintHandler = () => {
|
|
93
|
+
process.removeListener('SIGINT', sigintHandler)
|
|
92
94
|
child.kill('SIGTERM')
|
|
93
95
|
process.exit(0)
|
|
94
|
-
}
|
|
96
|
+
}
|
|
97
|
+
process.on('SIGINT', sigintHandler)
|
|
95
98
|
|
|
96
99
|
await new Promise<void>((resolve) => {
|
|
97
|
-
child.on('close', () =>
|
|
100
|
+
child.on('close', () => {
|
|
101
|
+
process.removeListener('SIGINT', sigintHandler)
|
|
102
|
+
resolve()
|
|
103
|
+
})
|
|
98
104
|
})
|
|
99
105
|
return
|
|
100
106
|
}
|
|
@@ -71,19 +71,19 @@ export async function handleCreateForRestore(): Promise<{
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const binarySpinner = createSpinner(
|
|
74
|
-
`Checking
|
|
74
|
+
`Checking ${dbEngine.displayName} ${version} binaries...`,
|
|
75
75
|
)
|
|
76
76
|
binarySpinner.start()
|
|
77
77
|
|
|
78
78
|
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
79
79
|
if (isInstalled) {
|
|
80
|
-
binarySpinner.succeed(
|
|
80
|
+
binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries ready (cached)`)
|
|
81
81
|
} else {
|
|
82
|
-
binarySpinner.text = `Downloading
|
|
82
|
+
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
83
83
|
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
84
84
|
binarySpinner.text = message
|
|
85
85
|
})
|
|
86
|
-
binarySpinner.succeed(
|
|
86
|
+
binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries downloaded`)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
while (await containerManager.exists(containerName)) {
|
|
@@ -112,7 +112,7 @@ export async function handleCreateForRestore(): Promise<{
|
|
|
112
112
|
|
|
113
113
|
initSpinner.succeed('Database cluster initialized')
|
|
114
114
|
|
|
115
|
-
const startSpinner = createSpinner(
|
|
115
|
+
const startSpinner = createSpinner(`Starting ${dbEngine.displayName}...`)
|
|
116
116
|
startSpinner.start()
|
|
117
117
|
|
|
118
118
|
const config = await containerManager.getConfig(containerName)
|
|
@@ -124,7 +124,7 @@ export async function handleCreateForRestore(): Promise<{
|
|
|
124
124
|
await dbEngine.start(config)
|
|
125
125
|
await containerManager.updateConfig(containerName, { status: 'running' })
|
|
126
126
|
|
|
127
|
-
startSpinner.succeed(
|
|
127
|
+
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
128
128
|
|
|
129
129
|
if (database !== 'postgres') {
|
|
130
130
|
const dbSpinner = createSpinner(`Creating database "${database}"...`)
|
|
@@ -259,11 +259,18 @@ export async function handleRestore(): Promise<void> {
|
|
|
259
259
|
message: 'Connection string:',
|
|
260
260
|
validate: (input: string) => {
|
|
261
261
|
if (!input) return true
|
|
262
|
-
if (
|
|
263
|
-
!input.startsWith('
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
if (config.engine === 'mysql') {
|
|
263
|
+
if (!input.startsWith('mysql://')) {
|
|
264
|
+
return 'Connection string must start with mysql://'
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// PostgreSQL
|
|
268
|
+
if (
|
|
269
|
+
!input.startsWith('postgresql://') &&
|
|
270
|
+
!input.startsWith('postgres://')
|
|
271
|
+
) {
|
|
272
|
+
return 'Connection string must start with postgresql:// or postgres://'
|
|
273
|
+
}
|
|
267
274
|
}
|
|
268
275
|
return true
|
|
269
276
|
},
|
|
@@ -300,15 +307,21 @@ export async function handleRestore(): Promise<void> {
|
|
|
300
307
|
|
|
301
308
|
if (
|
|
302
309
|
e.message.includes('pg_dump not found') ||
|
|
310
|
+
e.message.includes('mysqldump not found') ||
|
|
303
311
|
e.message.includes('ENOENT')
|
|
304
312
|
) {
|
|
305
|
-
const
|
|
313
|
+
const missingTool = e.message.includes('mysqldump')
|
|
314
|
+
? 'mysqldump'
|
|
315
|
+
: 'pg_dump'
|
|
316
|
+
const toolEngine = missingTool === 'mysqldump' ? 'mysql' : 'postgresql'
|
|
317
|
+
const installed = await promptInstallDependencies(missingTool, toolEngine as Engine)
|
|
306
318
|
if (installed) {
|
|
307
319
|
continue
|
|
308
320
|
}
|
|
309
321
|
} else {
|
|
322
|
+
const dumpTool = config.engine === 'mysql' ? 'mysqldump' : 'pg_dump'
|
|
310
323
|
console.log()
|
|
311
|
-
console.log(error(
|
|
324
|
+
console.log(error(`${dumpTool} error:`))
|
|
312
325
|
console.log(chalk.gray(` ${e.message}`))
|
|
313
326
|
console.log()
|
|
314
327
|
}
|
|
@@ -390,7 +403,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
390
403
|
createDatabase: false,
|
|
391
404
|
})
|
|
392
405
|
|
|
393
|
-
if (result.code === 0
|
|
406
|
+
if (result.code === 0) {
|
|
394
407
|
restoreSpinner.succeed('Backup restored successfully')
|
|
395
408
|
} else {
|
|
396
409
|
const stderr = result.stderr || ''
|
|
@@ -539,7 +552,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
539
552
|
}
|
|
540
553
|
}
|
|
541
554
|
|
|
542
|
-
if (result.code === 0
|
|
555
|
+
if (result.code === 0) {
|
|
543
556
|
const connectionString = engine.getConnectionString(config, databaseName)
|
|
544
557
|
console.log()
|
|
545
558
|
console.log(success(`Database "${databaseName}" restored`))
|
|
@@ -720,6 +733,12 @@ export async function handleClone(): Promise<void> {
|
|
|
720
733
|
)
|
|
721
734
|
if (!sourceName) return
|
|
722
735
|
|
|
736
|
+
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
737
|
+
if (!sourceConfig) {
|
|
738
|
+
console.log(error(`Container "${sourceName}" not found`))
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
723
742
|
const { targetName } = await inquirer.prompt<{ targetName: string }>([
|
|
724
743
|
{
|
|
725
744
|
type: 'input',
|
|
@@ -736,16 +755,28 @@ export async function handleClone(): Promise<void> {
|
|
|
736
755
|
},
|
|
737
756
|
])
|
|
738
757
|
|
|
758
|
+
// Check if target container already exists
|
|
759
|
+
if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
|
|
760
|
+
console.log(error(`Container "${targetName}" already exists`))
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
739
764
|
const spinner = createSpinner(`Cloning ${sourceName} to ${targetName}...`)
|
|
740
765
|
spinner.start()
|
|
741
766
|
|
|
742
|
-
|
|
767
|
+
try {
|
|
768
|
+
const newConfig = await containerManager.clone(sourceName, targetName)
|
|
743
769
|
|
|
744
|
-
|
|
770
|
+
spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
|
|
745
771
|
|
|
746
|
-
|
|
747
|
-
|
|
772
|
+
const engine = getEngine(newConfig.engine)
|
|
773
|
+
const connectionString = engine.getConnectionString(newConfig)
|
|
748
774
|
|
|
749
|
-
|
|
750
|
-
|
|
775
|
+
console.log()
|
|
776
|
+
console.log(connectionBox(targetName, connectionString, newConfig.port))
|
|
777
|
+
} catch (err) {
|
|
778
|
+
const e = err as Error
|
|
779
|
+
spinner.fail(`Failed to clone "${sourceName}"`)
|
|
780
|
+
console.log(error(e.message))
|
|
781
|
+
}
|
|
751
782
|
}
|