spindb 0.9.0 â 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 +7 -0
- package/cli/commands/clone.ts +6 -0
- package/cli/commands/create.ts +64 -14
- package/cli/commands/doctor.ts +5 -4
- package/cli/commands/edit.ts +7 -5
- package/cli/commands/engines.ts +34 -3
- package/cli/commands/info.ts +4 -2
- package/cli/commands/list.ts +4 -4
- package/cli/commands/logs.ts +9 -3
- package/cli/commands/menu/backup-handlers.ts +26 -8
- package/cli/commands/menu/container-handlers.ts +25 -7
- package/cli/commands/menu/engine-handlers.ts +128 -4
- package/cli/commands/restore.ts +83 -23
- package/cli/helpers.ts +41 -1
- package/cli/ui/prompts.ts +9 -3
- package/core/container-manager.ts +81 -30
- package/core/error-handler.ts +31 -0
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- 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 +13 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,6 +227,9 @@ spindb create mydb --port 5433 # Custom port
|
|
|
227
227
|
spindb create mydb --database my_app # Custom database name
|
|
228
228
|
spindb create mydb --no-start # Create without starting
|
|
229
229
|
|
|
230
|
+
# Create, start, and connect in one command
|
|
231
|
+
spindb create mydb --start --connect
|
|
232
|
+
|
|
230
233
|
# SQLite with custom path
|
|
231
234
|
spindb create mydb --engine sqlite --path ./data/app.sqlite
|
|
232
235
|
```
|
|
@@ -250,7 +253,9 @@ spindb create mydb --from "postgresql://user:pass@host:5432/production"
|
|
|
250
253
|
| `--path` | File path for SQLite databases |
|
|
251
254
|
| `--max-connections` | Maximum database connections (default: 200) |
|
|
252
255
|
| `--from` | Restore from backup file or connection string |
|
|
256
|
+
| `--start` | Start container after creation (skip prompt) |
|
|
253
257
|
| `--no-start` | Create without starting |
|
|
258
|
+
| `--connect` | Open a shell connection after creation |
|
|
254
259
|
|
|
255
260
|
</details>
|
|
256
261
|
|
|
@@ -400,10 +405,12 @@ ENGINE VERSION SOURCE SIZE
|
|
|
400
405
|
ð postgresql 17.7 darwin-arm64 45.2 MB
|
|
401
406
|
ð postgresql 16.8 darwin-arm64 44.8 MB
|
|
402
407
|
ðŽ mysql 8.0.35 system (system-installed)
|
|
408
|
+
ðŠķ sqlite 3.43.2 system (system-installed)
|
|
403
409
|
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
404
410
|
|
|
405
411
|
PostgreSQL: 2 version(s), 90.0 MB
|
|
406
412
|
MySQL: system-installed at /opt/homebrew/bin/mysqld
|
|
413
|
+
SQLite: system-installed at /usr/bin/sqlite3
|
|
407
414
|
```
|
|
408
415
|
|
|
409
416
|
#### `deps` - Manage client tools
|
package/cli/commands/clone.ts
CHANGED
|
@@ -71,6 +71,12 @@ export const cloneCommand = new Command('clone')
|
|
|
71
71
|
targetName = await promptContainerName(`${sourceName}-copy`)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Check if target container already exists
|
|
75
|
+
if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
|
|
76
|
+
console.error(error(`Container "${targetName}" already exists`))
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
const cloneSpinner = createSpinner(
|
|
75
81
|
`Cloning ${sourceName} to ${targetName}...`,
|
|
76
82
|
)
|
package/cli/commands/create.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { getMissingDependencies } from '../../core/dependency-manager'
|
|
|
20
20
|
import { platformService } from '../../core/platform-service'
|
|
21
21
|
import { startWithRetry } from '../../core/start-with-retry'
|
|
22
22
|
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
|
+
import { isValidDatabaseName } from '../../core/error-handler'
|
|
23
24
|
import { Engine } from '../../types'
|
|
24
25
|
import type { BaseEngine } from '../../engines/base-engine'
|
|
25
26
|
import { resolve } from 'path'
|
|
@@ -32,9 +33,9 @@ async function createSqliteContainer(
|
|
|
32
33
|
containerName: string,
|
|
33
34
|
dbEngine: BaseEngine,
|
|
34
35
|
version: string,
|
|
35
|
-
options: { path?: string; from?: string | null },
|
|
36
|
+
options: { path?: string; from?: string | null; connect?: boolean },
|
|
36
37
|
): Promise<void> {
|
|
37
|
-
const { path: filePath, from: restoreLocation } = options
|
|
38
|
+
const { path: filePath, from: restoreLocation, connect } = options
|
|
38
39
|
|
|
39
40
|
// Check dependencies
|
|
40
41
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
@@ -107,9 +108,20 @@ async function createSqliteContainer(
|
|
|
107
108
|
console.log(chalk.gray(' Connection string:'))
|
|
108
109
|
console.log(chalk.cyan(` sqlite:///${absolutePath}`))
|
|
109
110
|
console.log()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
|
|
112
|
+
// Connect if requested
|
|
113
|
+
if (connect) {
|
|
114
|
+
const config = await containerManager.getConfig(containerName)
|
|
115
|
+
if (config) {
|
|
116
|
+
console.log(chalk.gray(' Opening shell...'))
|
|
117
|
+
console.log()
|
|
118
|
+
await dbEngine.connect(config)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.gray(' Connect with:'))
|
|
122
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
123
|
+
console.log()
|
|
124
|
+
}
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
function detectLocationType(location: string): {
|
|
@@ -132,8 +144,9 @@ function detectLocationType(location: string): {
|
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
if (existsSync(location)) {
|
|
135
|
-
// Check if it's a SQLite file
|
|
136
|
-
|
|
147
|
+
// Check if it's a SQLite file (case-insensitive)
|
|
148
|
+
const lowerLocation = location.toLowerCase()
|
|
149
|
+
if (lowerLocation.endsWith('.sqlite') || lowerLocation.endsWith('.db') || lowerLocation.endsWith('.sqlite3')) {
|
|
137
150
|
return { type: 'file', inferredEngine: Engine.SQLite }
|
|
138
151
|
}
|
|
139
152
|
return { type: 'file' }
|
|
@@ -157,7 +170,9 @@ export const createCommand = new Command('create')
|
|
|
157
170
|
'--max-connections <number>',
|
|
158
171
|
'Maximum number of database connections (default: 200)',
|
|
159
172
|
)
|
|
173
|
+
.option('--start', 'Start the container after creation (skip prompt)')
|
|
160
174
|
.option('--no-start', 'Do not start the container after creation')
|
|
175
|
+
.option('--connect', 'Open a shell connection after creation')
|
|
161
176
|
.option(
|
|
162
177
|
'--from <location>',
|
|
163
178
|
'Restore from a dump file or connection string after creation',
|
|
@@ -172,7 +187,8 @@ export const createCommand = new Command('create')
|
|
|
172
187
|
port?: string
|
|
173
188
|
path?: string
|
|
174
189
|
maxConnections?: string
|
|
175
|
-
start
|
|
190
|
+
start?: boolean
|
|
191
|
+
connect?: boolean
|
|
176
192
|
from?: string
|
|
177
193
|
},
|
|
178
194
|
) => {
|
|
@@ -238,6 +254,16 @@ export const createCommand = new Command('create')
|
|
|
238
254
|
|
|
239
255
|
database = database ?? containerName
|
|
240
256
|
|
|
257
|
+
// Validate database name to prevent SQL injection
|
|
258
|
+
if (!isValidDatabaseName(database)) {
|
|
259
|
+
console.error(
|
|
260
|
+
error(
|
|
261
|
+
'Database name must start with a letter and contain only letters, numbers, hyphens, and underscores',
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
process.exit(1)
|
|
265
|
+
}
|
|
266
|
+
|
|
241
267
|
console.log(header('Creating Database Container'))
|
|
242
268
|
console.log()
|
|
243
269
|
|
|
@@ -248,10 +274,21 @@ export const createCommand = new Command('create')
|
|
|
248
274
|
await createSqliteContainer(containerName, dbEngine, version, {
|
|
249
275
|
path: options.path,
|
|
250
276
|
from: restoreLocation,
|
|
277
|
+
connect: options.connect,
|
|
251
278
|
})
|
|
252
279
|
return
|
|
253
280
|
}
|
|
254
281
|
|
|
282
|
+
// For server databases, validate --connect with --no-start
|
|
283
|
+
if (options.connect && options.start === false) {
|
|
284
|
+
console.error(
|
|
285
|
+
error(
|
|
286
|
+
'Cannot use --no-start with --connect (connection requires running container)',
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
process.exit(1)
|
|
290
|
+
}
|
|
291
|
+
|
|
255
292
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
256
293
|
depsSpinner.start()
|
|
257
294
|
|
|
@@ -384,9 +421,12 @@ export const createCommand = new Command('create')
|
|
|
384
421
|
throw err
|
|
385
422
|
}
|
|
386
423
|
|
|
387
|
-
// --from requires start, --no-start skips, otherwise ask user
|
|
424
|
+
// --from requires start, --start forces start, --no-start skips, otherwise ask user
|
|
425
|
+
// --connect implies --start for server databases
|
|
388
426
|
let shouldStart = false
|
|
389
|
-
if (restoreLocation) {
|
|
427
|
+
if (restoreLocation || options.connect) {
|
|
428
|
+
shouldStart = true
|
|
429
|
+
} else if (options.start === true) {
|
|
390
430
|
shouldStart = true
|
|
391
431
|
} else if (options.start === false) {
|
|
392
432
|
shouldStart = false
|
|
@@ -542,7 +582,7 @@ export const createCommand = new Command('create')
|
|
|
542
582
|
createDatabase: false,
|
|
543
583
|
})
|
|
544
584
|
|
|
545
|
-
if (result.code === 0
|
|
585
|
+
if (result.code === 0) {
|
|
546
586
|
restoreSpinner.succeed('Backup restored successfully')
|
|
547
587
|
} else {
|
|
548
588
|
restoreSpinner.warn('Restore completed with warnings')
|
|
@@ -573,7 +613,17 @@ export const createCommand = new Command('create')
|
|
|
573
613
|
)
|
|
574
614
|
console.log()
|
|
575
615
|
|
|
576
|
-
if (shouldStart) {
|
|
616
|
+
if (options.connect && shouldStart) {
|
|
617
|
+
// --connect flag: open shell directly
|
|
618
|
+
const copied =
|
|
619
|
+
await platformService.copyToClipboard(connectionString)
|
|
620
|
+
if (copied) {
|
|
621
|
+
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
622
|
+
}
|
|
623
|
+
console.log(chalk.gray(' Opening shell...'))
|
|
624
|
+
console.log()
|
|
625
|
+
await dbEngine.connect(finalConfig, database)
|
|
626
|
+
} else if (shouldStart) {
|
|
577
627
|
console.log(chalk.gray(' Connect with:'))
|
|
578
628
|
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
579
629
|
|
|
@@ -582,12 +632,12 @@ export const createCommand = new Command('create')
|
|
|
582
632
|
if (copied) {
|
|
583
633
|
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
584
634
|
}
|
|
635
|
+
console.log()
|
|
585
636
|
} else {
|
|
586
637
|
console.log(chalk.gray(' Start the container:'))
|
|
587
638
|
console.log(chalk.cyan(` spindb start ${containerName}`))
|
|
639
|
+
console.log()
|
|
588
640
|
}
|
|
589
|
-
|
|
590
|
-
console.log()
|
|
591
641
|
}
|
|
592
642
|
} catch (err) {
|
|
593
643
|
const e = err as Error
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -247,10 +247,6 @@ export const doctorCommand = new Command('doctor')
|
|
|
247
247
|
.description('Check system health and fix common issues')
|
|
248
248
|
.option('--json', 'Output as JSON')
|
|
249
249
|
.action(async (options: { json?: boolean }) => {
|
|
250
|
-
console.log()
|
|
251
|
-
console.log(header('SpinDB Health Check'))
|
|
252
|
-
console.log()
|
|
253
|
-
|
|
254
250
|
const checks = [
|
|
255
251
|
await checkConfiguration(),
|
|
256
252
|
await checkContainers(),
|
|
@@ -265,6 +261,11 @@ export const doctorCommand = new Command('doctor')
|
|
|
265
261
|
return
|
|
266
262
|
}
|
|
267
263
|
|
|
264
|
+
// Human-readable output - print header first
|
|
265
|
+
console.log()
|
|
266
|
+
console.log(header('SpinDB Health Check'))
|
|
267
|
+
console.log()
|
|
268
|
+
|
|
268
269
|
// Display results
|
|
269
270
|
for (const check of checks) {
|
|
270
271
|
displayResult(check)
|
package/cli/commands/edit.ts
CHANGED
|
@@ -37,7 +37,7 @@ async function promptEditAction(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Only show config option for engines that support it
|
|
40
|
-
if (engine ===
|
|
40
|
+
if (engine === Engine.PostgreSQL) {
|
|
41
41
|
choices.push({ name: 'Edit database config (postgresql.conf)', value: 'config' })
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -201,7 +201,7 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
|
|
|
201
201
|
default: currentPath,
|
|
202
202
|
validate: (input: string) => {
|
|
203
203
|
if (!input.trim()) return 'Path is required'
|
|
204
|
-
const resolvedPath = resolve(input)
|
|
204
|
+
const resolvedPath = resolve(input).toLowerCase()
|
|
205
205
|
if (!resolvedPath.endsWith('.sqlite') && !resolvedPath.endsWith('.db') && !resolvedPath.endsWith('.sqlite3')) {
|
|
206
206
|
return 'Path should end with .sqlite, .sqlite3, or .db'
|
|
207
207
|
}
|
|
@@ -518,7 +518,7 @@ export const editCommand = new Command('edit')
|
|
|
518
518
|
// Handle config change
|
|
519
519
|
if (options.setConfig) {
|
|
520
520
|
// Only PostgreSQL supports config editing for now
|
|
521
|
-
if (config.engine !==
|
|
521
|
+
if (config.engine !== Engine.PostgreSQL) {
|
|
522
522
|
console.error(
|
|
523
523
|
error(`Config editing is only supported for PostgreSQL containers`),
|
|
524
524
|
)
|
|
@@ -556,10 +556,12 @@ export const editCommand = new Command('edit')
|
|
|
556
556
|
configKey,
|
|
557
557
|
configValue,
|
|
558
558
|
)
|
|
559
|
+
spinner.succeed(`Set ${configKey} = ${configValue}`)
|
|
560
|
+
} else {
|
|
561
|
+
spinner.fail('Config editing not supported for this engine')
|
|
562
|
+
process.exit(1)
|
|
559
563
|
}
|
|
560
564
|
|
|
561
|
-
spinner.succeed(`Set ${configKey} = ${configValue}`)
|
|
562
|
-
|
|
563
565
|
// Check if container is running and warn about restart
|
|
564
566
|
const running = await processManager.isRunning(containerName, {
|
|
565
567
|
engine: config.engine,
|
package/cli/commands/engines.ts
CHANGED
|
@@ -12,8 +12,17 @@ import {
|
|
|
12
12
|
getInstalledPostgresEngines,
|
|
13
13
|
type InstalledPostgresEngine,
|
|
14
14
|
type InstalledMysqlEngine,
|
|
15
|
+
type InstalledSqliteEngine,
|
|
15
16
|
} from '../helpers'
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Pad string to width, accounting for emoji taking 2 display columns
|
|
20
|
+
*/
|
|
21
|
+
function padWithEmoji(str: string, width: number): string {
|
|
22
|
+
// Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
|
|
23
|
+
const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
|
|
24
|
+
return str.padEnd(width + emojiCount)
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* List subcommand action
|
|
@@ -41,13 +50,16 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
41
50
|
return
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
// Separate
|
|
53
|
+
// Separate engines by type
|
|
45
54
|
const pgEngines = engines.filter(
|
|
46
55
|
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
47
56
|
)
|
|
48
57
|
const mysqlEngine = engines.find(
|
|
49
58
|
(e): e is InstalledMysqlEngine => e.engine === 'mysql',
|
|
50
59
|
)
|
|
60
|
+
const sqliteEngine = engines.find(
|
|
61
|
+
(e): e is InstalledSqliteEngine => e.engine === 'sqlite',
|
|
62
|
+
)
|
|
51
63
|
|
|
52
64
|
// Calculate total size for PostgreSQL
|
|
53
65
|
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
@@ -67,10 +79,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
67
79
|
for (const engine of pgEngines) {
|
|
68
80
|
const icon = getEngineIcon(engine.engine)
|
|
69
81
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
82
|
+
const engineDisplay = `${icon} ${engine.engine}`
|
|
70
83
|
|
|
71
84
|
console.log(
|
|
72
85
|
chalk.gray(' ') +
|
|
73
|
-
chalk.cyan(
|
|
86
|
+
chalk.cyan(padWithEmoji(engineDisplay, 13)) +
|
|
74
87
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
75
88
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
76
89
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -81,16 +94,31 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
81
94
|
if (mysqlEngine) {
|
|
82
95
|
const icon = ENGINE_ICONS.mysql
|
|
83
96
|
const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
|
|
97
|
+
const engineDisplay = `${icon} ${displayName}`
|
|
84
98
|
|
|
85
99
|
console.log(
|
|
86
100
|
chalk.gray(' ') +
|
|
87
|
-
chalk.cyan(
|
|
101
|
+
chalk.cyan(padWithEmoji(engineDisplay, 13)) +
|
|
88
102
|
chalk.yellow(mysqlEngine.version.padEnd(12)) +
|
|
89
103
|
chalk.gray('system'.padEnd(18)) +
|
|
90
104
|
chalk.gray('(system-installed)'),
|
|
91
105
|
)
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
// SQLite row
|
|
109
|
+
if (sqliteEngine) {
|
|
110
|
+
const icon = ENGINE_ICONS.sqlite
|
|
111
|
+
const engineDisplay = `${icon} sqlite`
|
|
112
|
+
|
|
113
|
+
console.log(
|
|
114
|
+
chalk.gray(' ') +
|
|
115
|
+
chalk.cyan(padWithEmoji(engineDisplay, 13)) +
|
|
116
|
+
chalk.yellow(sqliteEngine.version.padEnd(12)) +
|
|
117
|
+
chalk.gray('system'.padEnd(18)) +
|
|
118
|
+
chalk.gray('(system-installed)'),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
console.log(chalk.gray(' ' + 'â'.repeat(55)))
|
|
95
123
|
|
|
96
124
|
// Summary
|
|
@@ -105,6 +133,9 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
105
133
|
if (mysqlEngine) {
|
|
106
134
|
console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
|
|
107
135
|
}
|
|
136
|
+
if (sqliteEngine) {
|
|
137
|
+
console.log(chalk.gray(` SQLite: system-installed at ${sqliteEngine.path}`))
|
|
138
|
+
}
|
|
108
139
|
console.log()
|
|
109
140
|
}
|
|
110
141
|
|
package/cli/commands/info.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from 'commander'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
|
+
import { basename } from 'path'
|
|
5
6
|
import { containerManager } from '../../core/container-manager'
|
|
6
7
|
import { processManager } from '../../core/process-manager'
|
|
7
8
|
import { paths } from '../../config/paths'
|
|
@@ -197,8 +198,9 @@ async function displayAllContainersInfo(
|
|
|
197
198
|
// Show truncated file path for SQLite instead of port
|
|
198
199
|
let portOrPath: string
|
|
199
200
|
if (isSQLite) {
|
|
200
|
-
const fileName = container.database
|
|
201
|
-
|
|
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
|
|
202
204
|
} else {
|
|
203
205
|
portOrPath = String(container.port)
|
|
204
206
|
}
|
package/cli/commands/list.ts
CHANGED
|
@@ -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
|
|
|
@@ -107,8 +107,8 @@ export const listCommand = new Command('list')
|
|
|
107
107
|
let portOrPath: string
|
|
108
108
|
if (container.engine === Engine.SQLite) {
|
|
109
109
|
const fileName = basename(container.database)
|
|
110
|
-
// Truncate if longer than
|
|
111
|
-
portOrPath = fileName.length >
|
|
110
|
+
// Truncate if longer than 8 chars to fit in 8-char column
|
|
111
|
+
portOrPath = fileName.length > 8 ? fileName.slice(0, 7) + 'âĶ' : fileName
|
|
112
112
|
} else {
|
|
113
113
|
portOrPath = String(container.port)
|
|
114
114
|
}
|
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
|
}
|
|
@@ -403,7 +403,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
403
403
|
createDatabase: false,
|
|
404
404
|
})
|
|
405
405
|
|
|
406
|
-
if (result.code === 0
|
|
406
|
+
if (result.code === 0) {
|
|
407
407
|
restoreSpinner.succeed('Backup restored successfully')
|
|
408
408
|
} else {
|
|
409
409
|
const stderr = result.stderr || ''
|
|
@@ -552,7 +552,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
-
if (result.code === 0
|
|
555
|
+
if (result.code === 0) {
|
|
556
556
|
const connectionString = engine.getConnectionString(config, databaseName)
|
|
557
557
|
console.log()
|
|
558
558
|
console.log(success(`Database "${databaseName}" restored`))
|
|
@@ -733,6 +733,12 @@ export async function handleClone(): Promise<void> {
|
|
|
733
733
|
)
|
|
734
734
|
if (!sourceName) return
|
|
735
735
|
|
|
736
|
+
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
737
|
+
if (!sourceConfig) {
|
|
738
|
+
console.log(error(`Container "${sourceName}" not found`))
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
736
742
|
const { targetName } = await inquirer.prompt<{ targetName: string }>([
|
|
737
743
|
{
|
|
738
744
|
type: 'input',
|
|
@@ -749,16 +755,28 @@ export async function handleClone(): Promise<void> {
|
|
|
749
755
|
},
|
|
750
756
|
])
|
|
751
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
|
+
|
|
752
764
|
const spinner = createSpinner(`Cloning ${sourceName} to ${targetName}...`)
|
|
753
765
|
spinner.start()
|
|
754
766
|
|
|
755
|
-
|
|
767
|
+
try {
|
|
768
|
+
const newConfig = await containerManager.clone(sourceName, targetName)
|
|
756
769
|
|
|
757
|
-
|
|
770
|
+
spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
|
|
758
771
|
|
|
759
|
-
|
|
760
|
-
|
|
772
|
+
const engine = getEngine(newConfig.engine)
|
|
773
|
+
const connectionString = engine.getConnectionString(newConfig)
|
|
761
774
|
|
|
762
|
-
|
|
763
|
-
|
|
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
|
+
}
|
|
764
782
|
}
|
|
@@ -1043,6 +1043,12 @@ async function handleCloneFromSubmenu(
|
|
|
1043
1043
|
sourceName: string,
|
|
1044
1044
|
showMainMenu: () => Promise<void>,
|
|
1045
1045
|
): Promise<void> {
|
|
1046
|
+
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
1047
|
+
if (!sourceConfig) {
|
|
1048
|
+
console.log(error(`Container "${sourceName}" not found`))
|
|
1049
|
+
return
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1046
1052
|
const { targetName } = await inquirer.prompt<{ targetName: string }>([
|
|
1047
1053
|
{
|
|
1048
1054
|
type: 'input',
|
|
@@ -1059,20 +1065,32 @@ async function handleCloneFromSubmenu(
|
|
|
1059
1065
|
},
|
|
1060
1066
|
])
|
|
1061
1067
|
|
|
1068
|
+
// Check if target container already exists
|
|
1069
|
+
if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
|
|
1070
|
+
console.log(error(`Container "${targetName}" already exists`))
|
|
1071
|
+
return
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1062
1074
|
const spinner = createSpinner(`Cloning ${sourceName} to ${targetName}...`)
|
|
1063
1075
|
spinner.start()
|
|
1064
1076
|
|
|
1065
|
-
|
|
1077
|
+
try {
|
|
1078
|
+
const newConfig = await containerManager.clone(sourceName, targetName)
|
|
1066
1079
|
|
|
1067
|
-
|
|
1080
|
+
spinner.succeed(`Cloned "${sourceName}" to "${targetName}"`)
|
|
1068
1081
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1082
|
+
const engine = getEngine(newConfig.engine)
|
|
1083
|
+
const connectionString = engine.getConnectionString(newConfig)
|
|
1071
1084
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1085
|
+
console.log()
|
|
1086
|
+
console.log(connectionBox(targetName, connectionString, newConfig.port))
|
|
1074
1087
|
|
|
1075
|
-
|
|
1088
|
+
await showContainerSubmenu(targetName, showMainMenu)
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
spinner.fail(`Failed to clone "${sourceName}"`)
|
|
1091
|
+
console.log(error((err as Error).message))
|
|
1092
|
+
await pressEnterToContinue()
|
|
1093
|
+
}
|
|
1076
1094
|
}
|
|
1077
1095
|
|
|
1078
1096
|
async function handleDelete(containerName: string): Promise<void> {
|