spindb 0.9.1 → 0.9.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 +5 -8
- package/cli/commands/attach.ts +108 -0
- package/cli/commands/backup.ts +13 -11
- package/cli/commands/clone.ts +14 -10
- package/cli/commands/config.ts +29 -29
- package/cli/commands/connect.ts +51 -39
- package/cli/commands/create.ts +65 -32
- package/cli/commands/delete.ts +8 -8
- package/cli/commands/deps.ts +17 -15
- package/cli/commands/detach.ts +100 -0
- package/cli/commands/doctor.ts +27 -13
- package/cli/commands/edit.ts +120 -57
- package/cli/commands/engines.ts +17 -15
- package/cli/commands/info.ts +8 -6
- package/cli/commands/list.ts +127 -18
- package/cli/commands/logs.ts +15 -11
- package/cli/commands/menu/backup-handlers.ts +52 -47
- package/cli/commands/menu/container-handlers.ts +164 -79
- package/cli/commands/menu/engine-handlers.ts +21 -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 +22 -20
- package/cli/commands/run.ts +20 -18
- package/cli/commands/self-update.ts +5 -5
- package/cli/commands/sqlite.ts +247 -0
- 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 +9 -4
- package/cli/index.ts +6 -0
- package/cli/ui/prompts.ts +12 -5
- package/cli/ui/spinner.ts +4 -4
- package/cli/ui/theme.ts +4 -4
- package/config/paths.ts +0 -8
- package/core/binary-manager.ts +5 -1
- package/core/config-manager.ts +32 -0
- package/core/container-manager.ts +5 -5
- package/core/platform-service.ts +3 -3
- package/core/start-with-retry.ts +6 -6
- package/core/transaction-manager.ts +6 -6
- package/engines/mysql/backup.ts +37 -13
- package/engines/mysql/index.ts +11 -11
- 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 +7 -2
- package/engines/postgresql/restore.ts +2 -2
- package/engines/postgresql/version-validator.ts +2 -2
- package/engines/sqlite/index.ts +30 -15
- package/engines/sqlite/registry.ts +64 -33
- package/engines/sqlite/scanner.ts +99 -0
- package/package.json +4 -3
- package/types/index.ts +21 -1
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import inquirer from 'inquirer'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
statSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
} from 'fs'
|
|
4
11
|
import { dirname, basename, join, resolve } from 'path'
|
|
5
12
|
import { homedir } from 'os'
|
|
6
13
|
import { containerManager } from '../../../core/container-manager'
|
|
@@ -22,10 +29,10 @@ import {
|
|
|
22
29
|
import { createSpinner } from '../../ui/spinner'
|
|
23
30
|
import {
|
|
24
31
|
header,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
uiSuccess,
|
|
33
|
+
uiError,
|
|
34
|
+
uiWarning,
|
|
35
|
+
uiInfo,
|
|
29
36
|
connectionBox,
|
|
30
37
|
formatBytes,
|
|
31
38
|
} from '../../ui/theme'
|
|
@@ -69,7 +76,7 @@ export async function handleCreate(): Promise<void> {
|
|
|
69
76
|
missingDeps = await getMissingDependencies(engine)
|
|
70
77
|
if (missingDeps.length > 0) {
|
|
71
78
|
console.log(
|
|
72
|
-
|
|
79
|
+
uiError(
|
|
73
80
|
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
74
81
|
),
|
|
75
82
|
)
|
|
@@ -94,13 +101,17 @@ export async function handleCreate(): Promise<void> {
|
|
|
94
101
|
|
|
95
102
|
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
96
103
|
if (isInstalled) {
|
|
97
|
-
binarySpinner.succeed(
|
|
104
|
+
binarySpinner.succeed(
|
|
105
|
+
`${dbEngine.displayName} ${version} binaries ready (cached)`,
|
|
106
|
+
)
|
|
98
107
|
} else {
|
|
99
108
|
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
100
109
|
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
101
110
|
binarySpinner.text = message
|
|
102
111
|
})
|
|
103
|
-
binarySpinner.succeed(
|
|
112
|
+
binarySpinner.succeed(
|
|
113
|
+
`${dbEngine.displayName} ${version} binaries downloaded`,
|
|
114
|
+
)
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
|
|
@@ -131,7 +142,9 @@ export async function handleCreate(): Promise<void> {
|
|
|
131
142
|
path: sqlitePath, // SQLite file path (undefined for server databases)
|
|
132
143
|
})
|
|
133
144
|
|
|
134
|
-
initSpinner.succeed(
|
|
145
|
+
initSpinner.succeed(
|
|
146
|
+
isSQLite ? 'Database file created' : 'Database cluster initialized',
|
|
147
|
+
)
|
|
135
148
|
|
|
136
149
|
// SQLite: show file path, no start needed
|
|
137
150
|
if (isSQLite) {
|
|
@@ -139,13 +152,13 @@ export async function handleCreate(): Promise<void> {
|
|
|
139
152
|
if (config) {
|
|
140
153
|
const connectionString = dbEngine.getConnectionString(config)
|
|
141
154
|
console.log()
|
|
142
|
-
console.log(
|
|
155
|
+
console.log(uiSuccess('Database Created'))
|
|
143
156
|
console.log()
|
|
144
157
|
console.log(chalk.gray(` Container: ${containerName}`))
|
|
145
158
|
console.log(chalk.gray(` Engine: ${dbEngine.displayName} ${version}`))
|
|
146
159
|
console.log(chalk.gray(` File: ${config.database}`))
|
|
147
160
|
console.log()
|
|
148
|
-
console.log(
|
|
161
|
+
console.log(uiSuccess(`Available at ${config.database}`))
|
|
149
162
|
console.log()
|
|
150
163
|
console.log(chalk.gray(' Connection string:'))
|
|
151
164
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
@@ -187,7 +200,9 @@ export async function handleCreate(): Promise<void> {
|
|
|
187
200
|
|
|
188
201
|
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
189
202
|
|
|
190
|
-
|
|
203
|
+
// Skip creating 'postgres' database for PostgreSQL - it's created by initdb
|
|
204
|
+
// For other engines (MySQL, SQLite), allow creating a database named 'postgres'
|
|
205
|
+
if (config && !(config.engine === 'postgresql' && database === 'postgres')) {
|
|
191
206
|
const dbSpinner = createSpinner(`Creating database "${database}"...`)
|
|
192
207
|
dbSpinner.start()
|
|
193
208
|
|
|
@@ -199,14 +214,14 @@ export async function handleCreate(): Promise<void> {
|
|
|
199
214
|
if (config) {
|
|
200
215
|
const connectionString = dbEngine.getConnectionString(config)
|
|
201
216
|
console.log()
|
|
202
|
-
console.log(
|
|
217
|
+
console.log(uiSuccess('Database Created'))
|
|
203
218
|
console.log()
|
|
204
219
|
console.log(chalk.gray(` Container: ${containerName}`))
|
|
205
220
|
console.log(chalk.gray(` Engine: ${dbEngine.displayName} ${version}`))
|
|
206
221
|
console.log(chalk.gray(` Database: ${database}`))
|
|
207
222
|
console.log(chalk.gray(` Port: ${port}`))
|
|
208
223
|
console.log()
|
|
209
|
-
console.log(
|
|
224
|
+
console.log(uiSuccess(`Running on port ${port}`))
|
|
210
225
|
console.log()
|
|
211
226
|
console.log(chalk.gray(' Connection string:'))
|
|
212
227
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
@@ -235,12 +250,12 @@ export async function handleCreate(): Promise<void> {
|
|
|
235
250
|
} else {
|
|
236
251
|
console.log()
|
|
237
252
|
console.log(
|
|
238
|
-
|
|
253
|
+
uiWarning(
|
|
239
254
|
`Port ${port} is currently in use. Container created but not started.`,
|
|
240
255
|
),
|
|
241
256
|
)
|
|
242
257
|
console.log(
|
|
243
|
-
|
|
258
|
+
uiInfo(
|
|
244
259
|
`Start it later with: ${chalk.cyan(`spindb start ${containerName}`)}`,
|
|
245
260
|
),
|
|
246
261
|
)
|
|
@@ -257,7 +272,7 @@ export async function handleList(
|
|
|
257
272
|
|
|
258
273
|
if (containers.length === 0) {
|
|
259
274
|
console.log(
|
|
260
|
-
|
|
275
|
+
uiInfo('No containers found. Create one with the "Create" option.'),
|
|
261
276
|
)
|
|
262
277
|
console.log()
|
|
263
278
|
|
|
@@ -302,19 +317,20 @@ export async function handleList(
|
|
|
302
317
|
|
|
303
318
|
// SQLite uses available/missing, server databases use running/stopped
|
|
304
319
|
const statusDisplay = isSQLite
|
|
305
|
-
?
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
:
|
|
309
|
-
|
|
310
|
-
|
|
320
|
+
? container.status === 'running'
|
|
321
|
+
? chalk.blue('● available')
|
|
322
|
+
: chalk.gray('○ missing')
|
|
323
|
+
: container.status === 'running'
|
|
324
|
+
? chalk.green('● running')
|
|
325
|
+
: chalk.gray('○ stopped')
|
|
311
326
|
|
|
312
327
|
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
313
328
|
|
|
314
329
|
// Truncate name if too long
|
|
315
|
-
const displayName =
|
|
316
|
-
|
|
317
|
-
|
|
330
|
+
const displayName =
|
|
331
|
+
container.name.length > 15
|
|
332
|
+
? container.name.slice(0, 14) + '…'
|
|
333
|
+
: container.name
|
|
318
334
|
|
|
319
335
|
// SQLite shows dash instead of port
|
|
320
336
|
const portDisplay = isSQLite ? '—' : String(container.port)
|
|
@@ -338,7 +354,9 @@ export async function handleList(
|
|
|
338
354
|
|
|
339
355
|
const running = serverContainers.filter((c) => c.status === 'running').length
|
|
340
356
|
const stopped = serverContainers.filter((c) => c.status !== 'running').length
|
|
341
|
-
const available = sqliteContainers.filter(
|
|
357
|
+
const available = sqliteContainers.filter(
|
|
358
|
+
(c) => c.status === 'running',
|
|
359
|
+
).length
|
|
342
360
|
const missing = sqliteContainers.filter((c) => c.status !== 'running').length
|
|
343
361
|
|
|
344
362
|
const parts: string[] = []
|
|
@@ -346,13 +364,13 @@ export async function handleList(
|
|
|
346
364
|
parts.push(`${running} running, ${stopped} stopped`)
|
|
347
365
|
}
|
|
348
366
|
if (sqliteContainers.length > 0) {
|
|
349
|
-
parts.push(
|
|
367
|
+
parts.push(
|
|
368
|
+
`${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`,
|
|
369
|
+
)
|
|
350
370
|
}
|
|
351
371
|
|
|
352
372
|
console.log(
|
|
353
|
-
chalk.gray(
|
|
354
|
-
` ${containers.length} container(s): ${parts.join('; ')}`,
|
|
355
|
-
),
|
|
373
|
+
chalk.gray(` ${containers.length} container(s): ${parts.join('; ')}`),
|
|
356
374
|
)
|
|
357
375
|
|
|
358
376
|
console.log()
|
|
@@ -361,8 +379,12 @@ export async function handleList(
|
|
|
361
379
|
// Simpler selector - table already shows details
|
|
362
380
|
const statusLabel =
|
|
363
381
|
c.engine === Engine.SQLite
|
|
364
|
-
?
|
|
365
|
-
|
|
382
|
+
? c.status === 'running'
|
|
383
|
+
? chalk.blue('● available')
|
|
384
|
+
: chalk.gray('○ missing')
|
|
385
|
+
: c.status === 'running'
|
|
386
|
+
? chalk.green('● running')
|
|
387
|
+
: chalk.gray('○ stopped')
|
|
366
388
|
|
|
367
389
|
return {
|
|
368
390
|
name: `${c.name} ${statusLabel}`,
|
|
@@ -400,7 +422,7 @@ export async function showContainerSubmenu(
|
|
|
400
422
|
): Promise<void> {
|
|
401
423
|
const config = await containerManager.getConfig(containerName)
|
|
402
424
|
if (!config) {
|
|
403
|
-
console.error(
|
|
425
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
404
426
|
return
|
|
405
427
|
}
|
|
406
428
|
|
|
@@ -458,7 +480,11 @@ export async function showContainerSubmenu(
|
|
|
458
480
|
? `${chalk.blue('⌘')} Open shell`
|
|
459
481
|
: chalk.gray('⌘ Open shell'),
|
|
460
482
|
value: 'shell',
|
|
461
|
-
disabled: canOpenShell
|
|
483
|
+
disabled: canOpenShell
|
|
484
|
+
? false
|
|
485
|
+
: isSQLite
|
|
486
|
+
? 'Database file missing'
|
|
487
|
+
: 'Start container first',
|
|
462
488
|
})
|
|
463
489
|
|
|
464
490
|
// Run SQL - always enabled for SQLite (if file exists), server databases need to be running
|
|
@@ -468,7 +494,11 @@ export async function showContainerSubmenu(
|
|
|
468
494
|
? `${chalk.yellow('▷')} Run SQL file`
|
|
469
495
|
: chalk.gray('▷ Run SQL file'),
|
|
470
496
|
value: 'run-sql',
|
|
471
|
-
disabled: canRunSql
|
|
497
|
+
disabled: canRunSql
|
|
498
|
+
? false
|
|
499
|
+
: isSQLite
|
|
500
|
+
? 'Database file missing'
|
|
501
|
+
: 'Start container first',
|
|
472
502
|
})
|
|
473
503
|
|
|
474
504
|
// Edit container - SQLite can always edit (no running state), server databases must be stopped
|
|
@@ -491,9 +521,10 @@ export async function showContainerSubmenu(
|
|
|
491
521
|
disabled: canClone ? false : 'Stop container first',
|
|
492
522
|
})
|
|
493
523
|
|
|
494
|
-
actionChoices.push(
|
|
495
|
-
|
|
496
|
-
|
|
524
|
+
actionChoices.push({
|
|
525
|
+
name: `${chalk.magenta('⎘')} Copy connection string`,
|
|
526
|
+
value: 'copy',
|
|
527
|
+
})
|
|
497
528
|
|
|
498
529
|
// View logs - not available for SQLite (no log file)
|
|
499
530
|
if (!isSQLite) {
|
|
@@ -503,6 +534,14 @@ export async function showContainerSubmenu(
|
|
|
503
534
|
})
|
|
504
535
|
}
|
|
505
536
|
|
|
537
|
+
// Detach - only for SQLite (unregisters without deleting file)
|
|
538
|
+
if (isSQLite) {
|
|
539
|
+
actionChoices.push({
|
|
540
|
+
name: `${chalk.yellow('⊘')} Detach from SpinDB`,
|
|
541
|
+
value: 'detach',
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
506
545
|
// Delete container - SQLite can always delete, server databases must be stopped
|
|
507
546
|
const canDelete = isSQLite ? true : !isRunning
|
|
508
547
|
actionChoices.push({
|
|
@@ -577,6 +616,9 @@ export async function showContainerSubmenu(
|
|
|
577
616
|
await handleCopyConnectionString(containerName)
|
|
578
617
|
await showContainerSubmenu(containerName, showMainMenu)
|
|
579
618
|
return
|
|
619
|
+
case 'detach':
|
|
620
|
+
await handleDetachContainer(containerName, showMainMenu)
|
|
621
|
+
return // Return to list after detach
|
|
580
622
|
case 'delete':
|
|
581
623
|
await handleDelete(containerName)
|
|
582
624
|
return // Don't show submenu again after delete
|
|
@@ -596,7 +638,7 @@ export async function handleStart(): Promise<void> {
|
|
|
596
638
|
)
|
|
597
639
|
|
|
598
640
|
if (stopped.length === 0) {
|
|
599
|
-
console.log(
|
|
641
|
+
console.log(uiWarning('All containers are already running'))
|
|
600
642
|
return
|
|
601
643
|
}
|
|
602
644
|
|
|
@@ -609,7 +651,7 @@ export async function handleStart(): Promise<void> {
|
|
|
609
651
|
|
|
610
652
|
const config = await containerManager.getConfig(containerName)
|
|
611
653
|
if (!config) {
|
|
612
|
-
console.error(
|
|
654
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
613
655
|
return
|
|
614
656
|
}
|
|
615
657
|
|
|
@@ -617,7 +659,7 @@ export async function handleStart(): Promise<void> {
|
|
|
617
659
|
if (!portAvailable) {
|
|
618
660
|
const { port: newPort } = await portManager.findAvailablePort()
|
|
619
661
|
console.log(
|
|
620
|
-
|
|
662
|
+
uiWarning(`Port ${config.port} is in use, switching to port ${newPort}`),
|
|
621
663
|
)
|
|
622
664
|
config.port = newPort
|
|
623
665
|
await containerManager.updateConfig(containerName, { port: newPort })
|
|
@@ -647,7 +689,7 @@ export async function handleStop(): Promise<void> {
|
|
|
647
689
|
)
|
|
648
690
|
|
|
649
691
|
if (running.length === 0) {
|
|
650
|
-
console.log(
|
|
692
|
+
console.log(uiWarning('No running containers'))
|
|
651
693
|
return
|
|
652
694
|
}
|
|
653
695
|
|
|
@@ -660,7 +702,7 @@ export async function handleStop(): Promise<void> {
|
|
|
660
702
|
|
|
661
703
|
const config = await containerManager.getConfig(containerName)
|
|
662
704
|
if (!config) {
|
|
663
|
-
console.error(
|
|
705
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
664
706
|
return
|
|
665
707
|
}
|
|
666
708
|
|
|
@@ -678,25 +720,25 @@ export async function handleStop(): Promise<void> {
|
|
|
678
720
|
async function handleStartContainer(containerName: string): Promise<void> {
|
|
679
721
|
const config = await containerManager.getConfig(containerName)
|
|
680
722
|
if (!config) {
|
|
681
|
-
console.error(
|
|
723
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
682
724
|
return
|
|
683
725
|
}
|
|
684
726
|
|
|
685
727
|
const portAvailable = await portManager.isPortAvailable(config.port)
|
|
686
728
|
if (!portAvailable) {
|
|
687
729
|
console.log(
|
|
688
|
-
|
|
730
|
+
uiWarning(
|
|
689
731
|
`Port ${config.port} is in use. Stop the process using it or change this container's port.`,
|
|
690
732
|
),
|
|
691
733
|
)
|
|
692
734
|
console.log()
|
|
693
735
|
console.log(
|
|
694
|
-
|
|
736
|
+
uiInfo(
|
|
695
737
|
'Tip: If you installed MariaDB via apt, it may have started a system service.',
|
|
696
738
|
),
|
|
697
739
|
)
|
|
698
740
|
console.log(
|
|
699
|
-
|
|
741
|
+
uiInfo(
|
|
700
742
|
'Run: sudo systemctl stop mariadb && sudo systemctl disable mariadb',
|
|
701
743
|
),
|
|
702
744
|
)
|
|
@@ -718,18 +760,18 @@ async function handleStartContainer(containerName: string): Promise<void> {
|
|
|
718
760
|
console.log()
|
|
719
761
|
console.log(chalk.gray(' Connection string:'))
|
|
720
762
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
721
|
-
} catch (
|
|
763
|
+
} catch (error) {
|
|
722
764
|
spinner.fail(`Failed to start "${containerName}"`)
|
|
723
|
-
const e =
|
|
765
|
+
const e = error as Error
|
|
724
766
|
console.log()
|
|
725
|
-
console.log(
|
|
767
|
+
console.log(uiError(e.message))
|
|
726
768
|
|
|
727
769
|
const logPath = paths.getContainerLogPath(containerName, {
|
|
728
770
|
engine: config.engine,
|
|
729
771
|
})
|
|
730
772
|
if (existsSync(logPath)) {
|
|
731
773
|
console.log()
|
|
732
|
-
console.log(
|
|
774
|
+
console.log(uiInfo(`Check the log file for details: ${logPath}`))
|
|
733
775
|
}
|
|
734
776
|
}
|
|
735
777
|
}
|
|
@@ -737,7 +779,7 @@ async function handleStartContainer(containerName: string): Promise<void> {
|
|
|
737
779
|
async function handleStopContainer(containerName: string): Promise<void> {
|
|
738
780
|
const config = await containerManager.getConfig(containerName)
|
|
739
781
|
if (!config) {
|
|
740
|
-
console.error(
|
|
782
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
741
783
|
return
|
|
742
784
|
}
|
|
743
785
|
|
|
@@ -757,7 +799,7 @@ async function handleEditContainer(
|
|
|
757
799
|
): Promise<string | null> {
|
|
758
800
|
const config = await containerManager.getConfig(containerName)
|
|
759
801
|
if (!config) {
|
|
760
|
-
console.error(
|
|
802
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
761
803
|
return null
|
|
762
804
|
}
|
|
763
805
|
|
|
@@ -767,7 +809,9 @@ async function handleEditContainer(
|
|
|
767
809
|
console.log(header(`Edit: ${containerName}`))
|
|
768
810
|
console.log()
|
|
769
811
|
|
|
770
|
-
const editChoices: Array<
|
|
812
|
+
const editChoices: Array<
|
|
813
|
+
{ name: string; value: string } | inquirer.Separator
|
|
814
|
+
> = [
|
|
771
815
|
{
|
|
772
816
|
name: `Name: ${chalk.white(containerName)}`,
|
|
773
817
|
value: 'name',
|
|
@@ -833,12 +877,12 @@ async function handleEditContainer(
|
|
|
833
877
|
])
|
|
834
878
|
|
|
835
879
|
if (newName === containerName) {
|
|
836
|
-
console.log(
|
|
880
|
+
console.log(uiInfo('Name unchanged'))
|
|
837
881
|
return await handleEditContainer(containerName)
|
|
838
882
|
}
|
|
839
883
|
|
|
840
884
|
if (await containerManager.exists(newName)) {
|
|
841
|
-
console.log(
|
|
885
|
+
console.log(uiError(`Container "${newName}" already exists`))
|
|
842
886
|
return await handleEditContainer(containerName)
|
|
843
887
|
}
|
|
844
888
|
|
|
@@ -872,21 +916,21 @@ async function handleEditContainer(
|
|
|
872
916
|
])
|
|
873
917
|
|
|
874
918
|
if (newPort === config.port) {
|
|
875
|
-
console.log(
|
|
919
|
+
console.log(uiInfo('Port unchanged'))
|
|
876
920
|
return await handleEditContainer(containerName)
|
|
877
921
|
}
|
|
878
922
|
|
|
879
923
|
const portAvailable = await portManager.isPortAvailable(newPort)
|
|
880
924
|
if (!portAvailable) {
|
|
881
925
|
console.log(
|
|
882
|
-
|
|
926
|
+
uiWarning(
|
|
883
927
|
`Port ${newPort} is currently in use. You'll need to stop the process using it before starting this container.`,
|
|
884
928
|
),
|
|
885
929
|
)
|
|
886
930
|
}
|
|
887
931
|
|
|
888
932
|
await containerManager.updateConfig(containerName, { port: newPort })
|
|
889
|
-
console.log(
|
|
933
|
+
console.log(uiSuccess(`Changed port from ${config.port} to ${newPort}`))
|
|
890
934
|
|
|
891
935
|
// Continue editing
|
|
892
936
|
return await handleEditContainer(containerName)
|
|
@@ -928,40 +972,43 @@ async function handleEditContainer(
|
|
|
928
972
|
// - ends with /
|
|
929
973
|
// - exists and is a directory
|
|
930
974
|
// - doesn't have a database file extension (assume it's a directory path)
|
|
931
|
-
const isDirectory =
|
|
975
|
+
const isDirectory =
|
|
976
|
+
expandedPath.endsWith('/') ||
|
|
932
977
|
(existsSync(expandedPath) && statSync(expandedPath).isDirectory()) ||
|
|
933
978
|
!hasDbExtension
|
|
934
979
|
|
|
935
980
|
let finalPath: string
|
|
936
981
|
if (isDirectory) {
|
|
937
982
|
// Remove trailing slash if present, then append filename
|
|
938
|
-
const dirPath = expandedPath.endsWith('/')
|
|
983
|
+
const dirPath = expandedPath.endsWith('/')
|
|
984
|
+
? expandedPath.slice(0, -1)
|
|
985
|
+
: expandedPath
|
|
939
986
|
finalPath = join(dirPath, currentFileName)
|
|
940
987
|
} else {
|
|
941
988
|
finalPath = expandedPath
|
|
942
989
|
}
|
|
943
990
|
|
|
944
991
|
if (finalPath === config.database) {
|
|
945
|
-
console.log(
|
|
992
|
+
console.log(uiInfo('Location unchanged'))
|
|
946
993
|
return await handleEditContainer(containerName)
|
|
947
994
|
}
|
|
948
995
|
|
|
949
996
|
// Check if source file exists
|
|
950
997
|
if (!existsSync(config.database)) {
|
|
951
|
-
console.log(
|
|
998
|
+
console.log(uiError(`Source file not found: ${config.database}`))
|
|
952
999
|
return await handleEditContainer(containerName)
|
|
953
1000
|
}
|
|
954
1001
|
|
|
955
1002
|
// Check if destination already exists
|
|
956
1003
|
if (existsSync(finalPath)) {
|
|
957
|
-
console.log(
|
|
1004
|
+
console.log(uiError(`Destination file already exists: ${finalPath}`))
|
|
958
1005
|
return await handleEditContainer(containerName)
|
|
959
1006
|
}
|
|
960
1007
|
|
|
961
1008
|
// Check if destination directory exists
|
|
962
1009
|
const destDir = dirname(finalPath)
|
|
963
1010
|
if (!existsSync(destDir)) {
|
|
964
|
-
console.log(
|
|
1011
|
+
console.log(uiWarning(`Directory does not exist: ${destDir}`))
|
|
965
1012
|
const { createDir } = await inquirer.prompt<{ createDir: string }>([
|
|
966
1013
|
{
|
|
967
1014
|
type: 'list',
|
|
@@ -980,9 +1027,13 @@ async function handleEditContainer(
|
|
|
980
1027
|
|
|
981
1028
|
try {
|
|
982
1029
|
mkdirSync(destDir, { recursive: true })
|
|
983
|
-
console.log(
|
|
984
|
-
} catch (
|
|
985
|
-
console.log(
|
|
1030
|
+
console.log(uiSuccess(`Created directory: ${destDir}`))
|
|
1031
|
+
} catch (mkdirError) {
|
|
1032
|
+
console.log(
|
|
1033
|
+
uiError(
|
|
1034
|
+
`Failed to create directory: ${(mkdirError as Error).message}`,
|
|
1035
|
+
),
|
|
1036
|
+
)
|
|
986
1037
|
return await handleEditContainer(containerName)
|
|
987
1038
|
}
|
|
988
1039
|
}
|
|
@@ -1020,15 +1071,17 @@ async function handleEditContainer(
|
|
|
1020
1071
|
}
|
|
1021
1072
|
|
|
1022
1073
|
// Update the container config and SQLite registry
|
|
1023
|
-
await containerManager.updateConfig(containerName, {
|
|
1074
|
+
await containerManager.updateConfig(containerName, {
|
|
1075
|
+
database: finalPath,
|
|
1076
|
+
})
|
|
1024
1077
|
await sqliteRegistry.update(containerName, { filePath: finalPath })
|
|
1025
1078
|
spinner.succeed(`Moved database to ${finalPath}`)
|
|
1026
1079
|
|
|
1027
1080
|
// Wait for user to see success message before refreshing
|
|
1028
1081
|
await pressEnterToContinue()
|
|
1029
|
-
} catch (
|
|
1082
|
+
} catch (error) {
|
|
1030
1083
|
spinner.fail('Failed to move database file')
|
|
1031
|
-
console.log(
|
|
1084
|
+
console.log(uiError((error as Error).message))
|
|
1032
1085
|
await pressEnterToContinue()
|
|
1033
1086
|
}
|
|
1034
1087
|
|
|
@@ -1045,7 +1098,7 @@ async function handleCloneFromSubmenu(
|
|
|
1045
1098
|
): Promise<void> {
|
|
1046
1099
|
const sourceConfig = await containerManager.getConfig(sourceName)
|
|
1047
1100
|
if (!sourceConfig) {
|
|
1048
|
-
console.log(
|
|
1101
|
+
console.log(uiError(`Container "${sourceName}" not found`))
|
|
1049
1102
|
return
|
|
1050
1103
|
}
|
|
1051
1104
|
|
|
@@ -1066,8 +1119,10 @@ async function handleCloneFromSubmenu(
|
|
|
1066
1119
|
])
|
|
1067
1120
|
|
|
1068
1121
|
// Check if target container already exists
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1122
|
+
if (
|
|
1123
|
+
await containerManager.exists(targetName, { engine: sourceConfig.engine })
|
|
1124
|
+
) {
|
|
1125
|
+
console.log(uiError(`Container "${targetName}" already exists`))
|
|
1071
1126
|
return
|
|
1072
1127
|
}
|
|
1073
1128
|
|
|
@@ -1086,17 +1141,47 @@ async function handleCloneFromSubmenu(
|
|
|
1086
1141
|
console.log(connectionBox(targetName, connectionString, newConfig.port))
|
|
1087
1142
|
|
|
1088
1143
|
await showContainerSubmenu(targetName, showMainMenu)
|
|
1089
|
-
} catch (
|
|
1144
|
+
} catch (error) {
|
|
1090
1145
|
spinner.fail(`Failed to clone "${sourceName}"`)
|
|
1091
|
-
console.log(
|
|
1146
|
+
console.log(uiError((error as Error).message))
|
|
1092
1147
|
await pressEnterToContinue()
|
|
1093
1148
|
}
|
|
1094
1149
|
}
|
|
1095
1150
|
|
|
1151
|
+
async function handleDetachContainer(
|
|
1152
|
+
containerName: string,
|
|
1153
|
+
showMainMenu: () => Promise<void>,
|
|
1154
|
+
): Promise<void> {
|
|
1155
|
+
const confirmed = await promptConfirm(
|
|
1156
|
+
`Detach "${containerName}" from SpinDB? (file will be kept on disk)`,
|
|
1157
|
+
true,
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
if (!confirmed) {
|
|
1161
|
+
console.log(uiWarning('Cancelled'))
|
|
1162
|
+
await pressEnterToContinue()
|
|
1163
|
+
await showContainerSubmenu(containerName, showMainMenu)
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const entry = await sqliteRegistry.get(containerName)
|
|
1168
|
+
await sqliteRegistry.remove(containerName)
|
|
1169
|
+
|
|
1170
|
+
console.log(uiSuccess(`Detached "${containerName}" from SpinDB`))
|
|
1171
|
+
if (entry?.filePath) {
|
|
1172
|
+
console.log(chalk.gray(` File remains at: ${entry.filePath}`))
|
|
1173
|
+
console.log()
|
|
1174
|
+
console.log(chalk.gray(' Re-attach with:'))
|
|
1175
|
+
console.log(chalk.cyan(` spindb attach ${entry.filePath}`))
|
|
1176
|
+
}
|
|
1177
|
+
await pressEnterToContinue()
|
|
1178
|
+
await handleList(showMainMenu)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1096
1181
|
async function handleDelete(containerName: string): Promise<void> {
|
|
1097
1182
|
const config = await containerManager.getConfig(containerName)
|
|
1098
1183
|
if (!config) {
|
|
1099
|
-
console.error(
|
|
1184
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
1100
1185
|
return
|
|
1101
1186
|
}
|
|
1102
1187
|
|
|
@@ -1106,7 +1191,7 @@ async function handleDelete(containerName: string): Promise<void> {
|
|
|
1106
1191
|
)
|
|
1107
1192
|
|
|
1108
1193
|
if (!confirmed) {
|
|
1109
|
-
console.log(
|
|
1194
|
+
console.log(uiWarning('Deletion cancelled'))
|
|
1110
1195
|
return
|
|
1111
1196
|
}
|
|
1112
1197
|
|