spindb 0.28.1 → 0.30.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 +32 -0
- package/cli/commands/create.ts +92 -26
- package/cli/commands/export.ts +362 -0
- package/cli/commands/menu/container-handlers.ts +276 -0
- package/cli/commands/self-update.ts +18 -6
- package/cli/commands/sqlite.ts +1 -3
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +5 -5
- package/core/credential-generator.ts +93 -0
- package/core/docker-exporter.ts +895 -0
- package/core/tls-generator.ts +116 -0
- package/core/update-manager.ts +25 -15
- package/engines/clickhouse/README.md +231 -0
- package/engines/cockroachdb/README.md +170 -0
- package/engines/couchdb/README.md +257 -0
- package/engines/duckdb/README.md +154 -0
- package/engines/ferretdb/README.md +220 -0
- package/engines/mariadb/README.md +141 -0
- package/engines/mariadb/backup.ts +2 -4
- package/engines/meilisearch/README.md +255 -0
- package/engines/mongodb/README.md +162 -0
- package/engines/mongodb/backup.ts +2 -2
- package/engines/mongodb/cli-utils.ts +107 -14
- package/engines/mongodb/index.ts +2 -1
- package/engines/mongodb/restore.ts +13 -6
- package/engines/mysql/README.md +142 -0
- package/engines/mysql/backup.ts +66 -9
- package/engines/mysql/index.ts +1 -0
- package/engines/mysql/restore.ts +56 -12
- package/engines/postgresql/README.md +158 -0
- package/engines/postgresql/backup.ts +70 -14
- package/engines/postgresql/index.ts +1 -0
- package/engines/postgresql/restore.ts +129 -18
- package/engines/qdrant/README.md +222 -0
- package/engines/qdrant/cli-utils.ts +2 -4
- package/engines/questdb/README.md +334 -0
- package/engines/questdb/index.ts +1 -2
- package/engines/redis/README.md +173 -0
- package/engines/redis/cli-utils.ts +2 -4
- package/engines/sqlite/README.md +162 -0
- package/engines/surrealdb/README.md +218 -0
- package/engines/surrealdb/index.ts +1 -2
- package/engines/valkey/README.md +219 -0
- package/engines/valkey/cli-utils.ts +2 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -279,6 +279,7 @@ spindb create mydb --engine mongodb # MongoDB
|
|
|
279
279
|
spindb create mydb --engine mysql --db-version 8.0 # MySQL 8.0
|
|
280
280
|
spindb create mydb --port 5433 # Custom port
|
|
281
281
|
spindb create mydb --start --connect # Create, start, and connect
|
|
282
|
+
spindb create mydb --force # Overwrite existing container
|
|
282
283
|
|
|
283
284
|
# Start/stop databases
|
|
284
285
|
spindb start mydb
|
|
@@ -354,6 +355,37 @@ spindb pull mydb --from-env PROD_URL --dry-run
|
|
|
354
355
|
spindb pull mydb --from-env PROD_URL --post-script ./sync-credentials.ts
|
|
355
356
|
```
|
|
356
357
|
|
|
358
|
+
### Export to Docker
|
|
359
|
+
|
|
360
|
+
Generate a Docker-ready package from any SpinDB container:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Export to Docker (generates Dockerfile, docker-compose.yml, etc.)
|
|
364
|
+
spindb export docker mydb
|
|
365
|
+
|
|
366
|
+
# Custom output directory
|
|
367
|
+
spindb export docker mydb -o ./deploy
|
|
368
|
+
|
|
369
|
+
# Override port (default: engine's standard port, e.g., 5432 for PostgreSQL)
|
|
370
|
+
spindb export docker mydb -p 5433
|
|
371
|
+
|
|
372
|
+
# Skip database backup or TLS certificates
|
|
373
|
+
spindb export docker mydb --no-data
|
|
374
|
+
spindb export docker mydb --no-tls
|
|
375
|
+
|
|
376
|
+
# JSON output for scripting
|
|
377
|
+
spindb export docker mydb --json --force
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Generated files:
|
|
381
|
+
- `Dockerfile` - Ubuntu 22.04 + Node.js 22 + SpinDB
|
|
382
|
+
- `docker-compose.yml` - Container orchestration
|
|
383
|
+
- `.env` - Auto-generated credentials
|
|
384
|
+
- `certs/` - TLS certificates (self-signed)
|
|
385
|
+
- `data/` - Database backup
|
|
386
|
+
- `entrypoint.sh` - Startup script
|
|
387
|
+
- `README.md` - Instructions
|
|
388
|
+
|
|
357
389
|
### Container Management
|
|
358
390
|
|
|
359
391
|
```bash
|
package/cli/commands/create.ts
CHANGED
|
@@ -37,10 +37,17 @@ async function createSqliteContainer(
|
|
|
37
37
|
path?: string
|
|
38
38
|
from?: string | null
|
|
39
39
|
connect?: boolean
|
|
40
|
+
force?: boolean
|
|
40
41
|
json?: boolean
|
|
41
42
|
},
|
|
42
43
|
): Promise<void> {
|
|
43
|
-
const {
|
|
44
|
+
const {
|
|
45
|
+
path: filePath,
|
|
46
|
+
from: restoreLocation,
|
|
47
|
+
connect,
|
|
48
|
+
force,
|
|
49
|
+
json,
|
|
50
|
+
} = options
|
|
44
51
|
|
|
45
52
|
// Check dependencies
|
|
46
53
|
const depsSpinner = json ? null : createSpinner('Checking required tools...')
|
|
@@ -70,17 +77,26 @@ async function createSqliteContainer(
|
|
|
70
77
|
|
|
71
78
|
// Check if container already exists
|
|
72
79
|
if (await containerManager.exists(containerName)) {
|
|
73
|
-
if (
|
|
80
|
+
if (force) {
|
|
81
|
+
// Delete existing container with force
|
|
82
|
+
if (!json) {
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.yellow(` Removing existing container "${containerName}"...`),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
await containerManager.delete(containerName, { force: true })
|
|
88
|
+
} else if (json) {
|
|
74
89
|
return exitWithError({
|
|
75
|
-
message: `Container "${containerName}" already exists
|
|
90
|
+
message: `Container "${containerName}" already exists. Use --force to overwrite.`,
|
|
76
91
|
json: true,
|
|
77
92
|
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
} else {
|
|
94
|
+
while (await containerManager.exists(containerName)) {
|
|
95
|
+
console.log(
|
|
96
|
+
chalk.yellow(` Container "${containerName}" already exists.`),
|
|
97
|
+
)
|
|
98
|
+
containerName = await promptContainerName()
|
|
99
|
+
}
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
|
|
@@ -190,10 +206,17 @@ async function createDuckDBContainer(
|
|
|
190
206
|
path?: string
|
|
191
207
|
from?: string | null
|
|
192
208
|
connect?: boolean
|
|
209
|
+
force?: boolean
|
|
193
210
|
json?: boolean
|
|
194
211
|
},
|
|
195
212
|
): Promise<void> {
|
|
196
|
-
const {
|
|
213
|
+
const {
|
|
214
|
+
path: filePath,
|
|
215
|
+
from: restoreLocation,
|
|
216
|
+
connect,
|
|
217
|
+
force,
|
|
218
|
+
json,
|
|
219
|
+
} = options
|
|
197
220
|
|
|
198
221
|
// Check dependencies
|
|
199
222
|
const depsSpinner = json ? null : createSpinner('Checking required tools...')
|
|
@@ -223,17 +246,26 @@ async function createDuckDBContainer(
|
|
|
223
246
|
|
|
224
247
|
// Check if container already exists
|
|
225
248
|
if (await containerManager.exists(containerName)) {
|
|
226
|
-
if (
|
|
249
|
+
if (force) {
|
|
250
|
+
// Delete existing container with force
|
|
251
|
+
if (!json) {
|
|
252
|
+
console.log(
|
|
253
|
+
chalk.yellow(` Removing existing container "${containerName}"...`),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
await containerManager.delete(containerName, { force: true })
|
|
257
|
+
} else if (json) {
|
|
227
258
|
return exitWithError({
|
|
228
|
-
message: `Container "${containerName}" already exists
|
|
259
|
+
message: `Container "${containerName}" already exists. Use --force to overwrite.`,
|
|
229
260
|
json: true,
|
|
230
261
|
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
} else {
|
|
263
|
+
while (await containerManager.exists(containerName)) {
|
|
264
|
+
console.log(
|
|
265
|
+
chalk.yellow(` Container "${containerName}" already exists.`),
|
|
266
|
+
)
|
|
267
|
+
containerName = await promptContainerName()
|
|
268
|
+
}
|
|
237
269
|
}
|
|
238
270
|
}
|
|
239
271
|
|
|
@@ -404,6 +436,10 @@ export const createCommand = new Command('create')
|
|
|
404
436
|
'--max-connections <number>',
|
|
405
437
|
'Maximum number of database connections (default: 200)',
|
|
406
438
|
)
|
|
439
|
+
.option(
|
|
440
|
+
'-f, --force',
|
|
441
|
+
'Overwrite existing container without prompting (deletes existing data)',
|
|
442
|
+
)
|
|
407
443
|
.option('--start', 'Start the container after creation (skip prompt)')
|
|
408
444
|
.option('--no-start', 'Do not start the container after creation')
|
|
409
445
|
.option('--connect', 'Open a shell connection after creation')
|
|
@@ -422,6 +458,7 @@ export const createCommand = new Command('create')
|
|
|
422
458
|
port?: string
|
|
423
459
|
path?: string
|
|
424
460
|
maxConnections?: string
|
|
461
|
+
force?: boolean
|
|
425
462
|
start?: boolean
|
|
426
463
|
connect?: boolean
|
|
427
464
|
from?: string
|
|
@@ -541,6 +578,7 @@ export const createCommand = new Command('create')
|
|
|
541
578
|
path: options.path,
|
|
542
579
|
from: restoreLocation,
|
|
543
580
|
connect: options.connect,
|
|
581
|
+
force: options.force,
|
|
544
582
|
json: options.json,
|
|
545
583
|
})
|
|
546
584
|
return
|
|
@@ -552,6 +590,7 @@ export const createCommand = new Command('create')
|
|
|
552
590
|
path: options.path,
|
|
553
591
|
from: restoreLocation,
|
|
554
592
|
connect: options.connect,
|
|
593
|
+
force: options.force,
|
|
555
594
|
json: options.json,
|
|
556
595
|
})
|
|
557
596
|
return
|
|
@@ -733,17 +772,44 @@ export const createCommand = new Command('create')
|
|
|
733
772
|
}
|
|
734
773
|
|
|
735
774
|
if (await containerManager.exists(containerName)) {
|
|
736
|
-
if (options.
|
|
775
|
+
if (options.force) {
|
|
776
|
+
// Stop the container if it's running, then delete it
|
|
777
|
+
const existingConfig =
|
|
778
|
+
await containerManager.getConfig(containerName)
|
|
779
|
+
if (existingConfig?.status === 'running') {
|
|
780
|
+
if (!options.json) {
|
|
781
|
+
console.log(
|
|
782
|
+
chalk.yellow(
|
|
783
|
+
` Stopping existing container "${containerName}"...`,
|
|
784
|
+
),
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
await dbEngine.stop(existingConfig)
|
|
789
|
+
} catch {
|
|
790
|
+
// Ignore stop errors - container may already be stopped
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!options.json) {
|
|
794
|
+
console.log(
|
|
795
|
+
chalk.yellow(
|
|
796
|
+
` Removing existing container "${containerName}"...`,
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
await containerManager.delete(containerName, { force: true })
|
|
801
|
+
} else if (options.json) {
|
|
737
802
|
return exitWithError({
|
|
738
|
-
message: `Container "${containerName}" already exists
|
|
803
|
+
message: `Container "${containerName}" already exists. Use --force to overwrite.`,
|
|
739
804
|
json: true,
|
|
740
805
|
})
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
806
|
+
} else {
|
|
807
|
+
while (await containerManager.exists(containerName)) {
|
|
808
|
+
console.log(
|
|
809
|
+
chalk.yellow(` Container "${containerName}" already exists.`),
|
|
810
|
+
)
|
|
811
|
+
containerName = await promptContainerName()
|
|
812
|
+
}
|
|
747
813
|
}
|
|
748
814
|
}
|
|
749
815
|
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { join, resolve } from 'path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { containerManager } from '../../core/container-manager'
|
|
5
|
+
import { processManager } from '../../core/process-manager'
|
|
6
|
+
import { getEngine } from '../../engines'
|
|
7
|
+
import { platformService } from '../../core/platform-service'
|
|
8
|
+
import { exportToDocker, getExportBackupPath } from '../../core/docker-exporter'
|
|
9
|
+
import { promptContainerSelect, promptConfirm } from '../ui/prompts'
|
|
10
|
+
import { createSpinner } from '../ui/spinner'
|
|
11
|
+
import { uiSuccess, uiError, uiWarning, box, formatBytes } from '../ui/theme'
|
|
12
|
+
import { isFileBasedEngine } from '../../types'
|
|
13
|
+
import { getDefaultFormat } from '../../config/backup-formats'
|
|
14
|
+
import { getEngineDefaults } from '../../config/engine-defaults'
|
|
15
|
+
import { paths } from '../../config/paths'
|
|
16
|
+
import { stat, rm, mkdir } from 'fs/promises'
|
|
17
|
+
import { existsSync } from 'fs'
|
|
18
|
+
import inquirer from 'inquirer'
|
|
19
|
+
|
|
20
|
+
export const exportCommand = new Command('export')
|
|
21
|
+
.description('Export container to various formats')
|
|
22
|
+
.addCommand(
|
|
23
|
+
new Command('docker')
|
|
24
|
+
.description('Export container to Docker-ready package')
|
|
25
|
+
.argument('[container]', 'Container name')
|
|
26
|
+
.option(
|
|
27
|
+
'-o, --output <dir>',
|
|
28
|
+
'Output directory (default: ~/.spindb/containers/{engine}/{name}/docker)',
|
|
29
|
+
)
|
|
30
|
+
.option('-p, --port <number>', 'Override external port', parseInt)
|
|
31
|
+
.option('--no-data', 'Skip including database backup')
|
|
32
|
+
.option('--no-tls', 'Skip TLS certificate generation')
|
|
33
|
+
.option('-f, --force', 'Overwrite existing output directory')
|
|
34
|
+
.option('-c, --copy', 'Copy password to clipboard')
|
|
35
|
+
.option('-j, --json', 'Output result as JSON')
|
|
36
|
+
.action(
|
|
37
|
+
async (
|
|
38
|
+
containerArg: string | undefined,
|
|
39
|
+
options: {
|
|
40
|
+
output?: string
|
|
41
|
+
port?: number
|
|
42
|
+
data?: boolean
|
|
43
|
+
tls?: boolean
|
|
44
|
+
force?: boolean
|
|
45
|
+
copy?: boolean
|
|
46
|
+
json?: boolean
|
|
47
|
+
},
|
|
48
|
+
) => {
|
|
49
|
+
try {
|
|
50
|
+
let containerName = containerArg
|
|
51
|
+
// Track if we're in interactive mode (no container arg = user will select)
|
|
52
|
+
const isInteractive = !containerArg && !options.json
|
|
53
|
+
|
|
54
|
+
// Select container if not provided
|
|
55
|
+
if (!containerName) {
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(
|
|
58
|
+
JSON.stringify({ error: 'Container name is required' }),
|
|
59
|
+
)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const containers = await containerManager.list()
|
|
64
|
+
|
|
65
|
+
if (containers.length === 0) {
|
|
66
|
+
console.log(
|
|
67
|
+
uiWarning(
|
|
68
|
+
'No containers found. Create one with: spindb create',
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const selected = await promptContainerSelect(
|
|
75
|
+
containers,
|
|
76
|
+
'Select container to export:',
|
|
77
|
+
)
|
|
78
|
+
if (!selected) return
|
|
79
|
+
containerName = selected
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get container config
|
|
83
|
+
const config = await containerManager.getConfig(containerName)
|
|
84
|
+
if (!config) {
|
|
85
|
+
if (options.json) {
|
|
86
|
+
console.log(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
error: `Container "${containerName}" not found`,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
} else {
|
|
92
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
93
|
+
}
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { engine: engineName, version, database, port } = config
|
|
98
|
+
const engine = getEngine(engineName)
|
|
99
|
+
const engineDefaultPort = getEngineDefaults(engineName).defaultPort
|
|
100
|
+
|
|
101
|
+
// Default output directory: ~/.spindb/containers/{engine}/{name}/docker
|
|
102
|
+
const defaultOutputDir = join(
|
|
103
|
+
paths.getContainerPath(containerName, { engine: engineName }),
|
|
104
|
+
'docker',
|
|
105
|
+
)
|
|
106
|
+
const outputDir = options.output
|
|
107
|
+
? resolve(options.output)
|
|
108
|
+
: defaultOutputDir
|
|
109
|
+
const includeData = options.data !== false
|
|
110
|
+
const skipTLS = options.tls === false
|
|
111
|
+
|
|
112
|
+
// Determine target port:
|
|
113
|
+
// 1. If user explicitly passed -p, use that
|
|
114
|
+
// 2. If local port matches engine default, use it
|
|
115
|
+
// 3. If interactive mode (no container arg) and ports differ, prompt user
|
|
116
|
+
// 4. CLI mode or JSON mode: default to engine's standard port
|
|
117
|
+
let targetPort: number
|
|
118
|
+
if (options.port !== undefined) {
|
|
119
|
+
// User explicitly specified a port
|
|
120
|
+
targetPort = options.port
|
|
121
|
+
} else if (port === engineDefaultPort) {
|
|
122
|
+
// Local port matches engine default, no decision needed
|
|
123
|
+
targetPort = engineDefaultPort
|
|
124
|
+
} else if (isInteractive) {
|
|
125
|
+
// Interactive mode only: prompt user to choose between local and default port
|
|
126
|
+
console.log()
|
|
127
|
+
console.log(
|
|
128
|
+
chalk.yellow(
|
|
129
|
+
`Local container uses port ${chalk.cyan(String(port))}, but ${engine.displayName}'s standard port is ${chalk.cyan(String(engineDefaultPort))}.`,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
const { selectedPort } = await inquirer.prompt<{
|
|
133
|
+
selectedPort: number
|
|
134
|
+
}>([
|
|
135
|
+
{
|
|
136
|
+
type: 'list',
|
|
137
|
+
name: 'selectedPort',
|
|
138
|
+
message: 'Which port should the Docker container use?',
|
|
139
|
+
choices: [
|
|
140
|
+
{
|
|
141
|
+
name: `${engineDefaultPort} ${chalk.gray('(standard port - recommended)')}`,
|
|
142
|
+
value: engineDefaultPort,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: `${port} ${chalk.gray('(same as local container)')}`,
|
|
146
|
+
value: port,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
default: engineDefaultPort,
|
|
150
|
+
},
|
|
151
|
+
])
|
|
152
|
+
targetPort = selectedPort
|
|
153
|
+
} else {
|
|
154
|
+
// CLI mode or JSON mode: default to standard port
|
|
155
|
+
targetPort = engineDefaultPort
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if output directory already exists
|
|
159
|
+
if (existsSync(outputDir)) {
|
|
160
|
+
let shouldOverwrite = options.force
|
|
161
|
+
|
|
162
|
+
if (!shouldOverwrite && !options.json) {
|
|
163
|
+
// Interactive prompt to confirm overwrite
|
|
164
|
+
console.log()
|
|
165
|
+
console.log(
|
|
166
|
+
uiWarning(`Output directory already exists: ${outputDir}`),
|
|
167
|
+
)
|
|
168
|
+
shouldOverwrite = await promptConfirm(
|
|
169
|
+
'Do you want to overwrite it?',
|
|
170
|
+
false, // Default to No for safety
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (shouldOverwrite) {
|
|
175
|
+
// Remove existing directory
|
|
176
|
+
await rm(outputDir, { recursive: true, force: true })
|
|
177
|
+
} else {
|
|
178
|
+
if (options.json) {
|
|
179
|
+
console.log(
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
error: `Output directory already exists: ${outputDir}`,
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
} else {
|
|
185
|
+
console.log(
|
|
186
|
+
uiError(
|
|
187
|
+
'Export cancelled. Use --force to overwrite or --output to specify a different path.',
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
process.exit(1)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// For server-based engines with data, check if container is running
|
|
196
|
+
let backupPath: string | undefined
|
|
197
|
+
if (includeData && !isFileBasedEngine(engineName)) {
|
|
198
|
+
const running = await processManager.isRunning(containerName, {
|
|
199
|
+
engine: engineName,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
if (!running) {
|
|
203
|
+
if (options.json) {
|
|
204
|
+
console.log(
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
error: `Container "${containerName}" is not running. Start it first to export with data.`,
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
} else {
|
|
210
|
+
console.error(
|
|
211
|
+
uiError(
|
|
212
|
+
`Container "${containerName}" is not running.\nStart it first with: spindb start ${containerName}`,
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!options.json) {
|
|
221
|
+
console.log()
|
|
222
|
+
console.log(
|
|
223
|
+
chalk.bold(
|
|
224
|
+
`Exporting ${chalk.cyan(containerName)} to Docker...`,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
console.log()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Step 1: Create backup if including data
|
|
231
|
+
if (includeData) {
|
|
232
|
+
const backupSpinner = options.json
|
|
233
|
+
? null
|
|
234
|
+
: createSpinner('Creating database backup...')
|
|
235
|
+
backupSpinner?.start()
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Create a temporary backup
|
|
239
|
+
const tempBackupPath = getExportBackupPath(
|
|
240
|
+
outputDir,
|
|
241
|
+
containerName,
|
|
242
|
+
database,
|
|
243
|
+
engineName,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// Create parent directory for backup
|
|
247
|
+
await mkdir(join(outputDir, 'data'), { recursive: true })
|
|
248
|
+
|
|
249
|
+
// Create backup using engine's backup method
|
|
250
|
+
const format = getDefaultFormat(engineName)
|
|
251
|
+
const result = await engine.backup(config, tempBackupPath, {
|
|
252
|
+
database,
|
|
253
|
+
format,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
backupPath = result.path
|
|
257
|
+
|
|
258
|
+
const backupStat = await stat(result.path)
|
|
259
|
+
backupSpinner?.succeed(
|
|
260
|
+
`Backup created (${formatBytes(backupStat.size)})`,
|
|
261
|
+
)
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const e = error as Error
|
|
264
|
+
backupSpinner?.fail('Backup failed')
|
|
265
|
+
|
|
266
|
+
if (options.json) {
|
|
267
|
+
console.log(JSON.stringify({ error: e.message }))
|
|
268
|
+
} else {
|
|
269
|
+
console.error(uiError(e.message))
|
|
270
|
+
}
|
|
271
|
+
process.exit(1)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Step 2: Generate Docker artifacts
|
|
276
|
+
const exportSpinner = options.json
|
|
277
|
+
? null
|
|
278
|
+
: createSpinner('Generating Docker artifacts...')
|
|
279
|
+
exportSpinner?.start()
|
|
280
|
+
|
|
281
|
+
const result = await exportToDocker(config, {
|
|
282
|
+
outputDir,
|
|
283
|
+
port: targetPort,
|
|
284
|
+
includeData,
|
|
285
|
+
backupPath,
|
|
286
|
+
skipTLS,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
exportSpinner?.succeed('Docker artifacts generated')
|
|
290
|
+
|
|
291
|
+
// Copy password to clipboard if requested
|
|
292
|
+
if (options.copy) {
|
|
293
|
+
const copied = await platformService.copyToClipboard(
|
|
294
|
+
result.credentials.password,
|
|
295
|
+
)
|
|
296
|
+
if (copied && !options.json) {
|
|
297
|
+
console.log(uiSuccess('Password copied to clipboard'))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Output results
|
|
302
|
+
if (options.json) {
|
|
303
|
+
console.log(
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
success: true,
|
|
306
|
+
outputDir: result.outputDir,
|
|
307
|
+
engine: result.engine,
|
|
308
|
+
version: result.version,
|
|
309
|
+
port: result.port,
|
|
310
|
+
database: result.database,
|
|
311
|
+
username: result.credentials.username,
|
|
312
|
+
password: result.credentials.password,
|
|
313
|
+
files: result.files,
|
|
314
|
+
}),
|
|
315
|
+
)
|
|
316
|
+
} else {
|
|
317
|
+
console.log()
|
|
318
|
+
console.log(
|
|
319
|
+
uiSuccess(`Exported ${chalk.cyan(containerName)} to Docker`),
|
|
320
|
+
)
|
|
321
|
+
console.log()
|
|
322
|
+
|
|
323
|
+
// Display summary box
|
|
324
|
+
const lines = [
|
|
325
|
+
`${chalk.bold(engine.displayName)} ${version}`,
|
|
326
|
+
`Port: ${chalk.green(String(targetPort))}`,
|
|
327
|
+
`Database: ${chalk.cyan(database)}`,
|
|
328
|
+
'',
|
|
329
|
+
chalk.bold('Generated Credentials'),
|
|
330
|
+
chalk.gray('────────────────────────'),
|
|
331
|
+
`Username: ${chalk.white(result.credentials.username)}`,
|
|
332
|
+
`Password: ${chalk.white(result.credentials.password)}`,
|
|
333
|
+
chalk.gray('────────────────────────'),
|
|
334
|
+
'',
|
|
335
|
+
chalk.yellow('Save these credentials now - stored in .env'),
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
console.log(box(lines))
|
|
339
|
+
console.log()
|
|
340
|
+
console.log(chalk.gray(' Output:'), chalk.cyan(result.outputDir))
|
|
341
|
+
console.log()
|
|
342
|
+
console.log(chalk.bold(' To run:'))
|
|
343
|
+
console.log(
|
|
344
|
+
chalk.cyan(
|
|
345
|
+
` cd "${result.outputDir}" && docker compose up -d`,
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
console.log()
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const e = error as Error
|
|
352
|
+
|
|
353
|
+
if (options.json) {
|
|
354
|
+
console.log(JSON.stringify({ error: e.message }))
|
|
355
|
+
} else {
|
|
356
|
+
console.error(uiError(e.message))
|
|
357
|
+
}
|
|
358
|
+
process.exit(1)
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
),
|
|
362
|
+
)
|