spindb 0.27.6 β†’ 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.
Files changed (59) hide show
  1. package/README.md +133 -47
  2. package/cli/commands/create.ts +92 -26
  3. package/cli/commands/databases.ts +278 -74
  4. package/cli/commands/export.ts +362 -0
  5. package/cli/commands/menu/backup-handlers.ts +62 -27
  6. package/cli/commands/menu/container-handlers.ts +641 -144
  7. package/cli/commands/menu/engine-handlers.ts +2 -2
  8. package/cli/commands/menu/index.ts +2 -1
  9. package/cli/commands/menu/shell-handlers.ts +69 -42
  10. package/cli/commands/menu/sql-handlers.ts +8 -18
  11. package/cli/commands/pull.ts +223 -0
  12. package/cli/commands/self-update.ts +18 -6
  13. package/cli/commands/sqlite.ts +1 -3
  14. package/cli/commands/which.ts +290 -0
  15. package/cli/constants.ts +10 -0
  16. package/cli/index.ts +6 -0
  17. package/cli/ui/prompts.ts +216 -44
  18. package/cli/ui/theme.ts +1 -6
  19. package/core/credential-generator.ts +93 -0
  20. package/core/docker-exporter.ts +895 -0
  21. package/core/pull-manager.ts +496 -0
  22. package/core/tls-generator.ts +116 -0
  23. package/core/update-manager.ts +25 -15
  24. package/engines/base-engine.ts +14 -0
  25. package/engines/clickhouse/README.md +231 -0
  26. package/engines/cockroachdb/README.md +170 -0
  27. package/engines/couchdb/README.md +257 -0
  28. package/engines/duckdb/README.md +154 -0
  29. package/engines/ferretdb/README.md +220 -0
  30. package/engines/mariadb/README.md +141 -0
  31. package/engines/mariadb/backup.ts +2 -4
  32. package/engines/mariadb/index.ts +47 -0
  33. package/engines/meilisearch/README.md +255 -0
  34. package/engines/mongodb/README.md +162 -0
  35. package/engines/mongodb/backup.ts +2 -2
  36. package/engines/mongodb/cli-utils.ts +107 -14
  37. package/engines/mongodb/index.ts +2 -1
  38. package/engines/mongodb/restore.ts +13 -6
  39. package/engines/mysql/README.md +142 -0
  40. package/engines/mysql/backup.ts +66 -9
  41. package/engines/mysql/index.ts +48 -0
  42. package/engines/mysql/restore.ts +56 -12
  43. package/engines/postgresql/README.md +158 -0
  44. package/engines/postgresql/backup.ts +70 -14
  45. package/engines/postgresql/index.ts +26 -0
  46. package/engines/postgresql/restore.ts +129 -18
  47. package/engines/qdrant/README.md +222 -0
  48. package/engines/qdrant/cli-utils.ts +2 -4
  49. package/engines/questdb/README.md +334 -0
  50. package/engines/questdb/index.ts +1 -2
  51. package/engines/redis/README.md +173 -0
  52. package/engines/redis/cli-utils.ts +2 -4
  53. package/engines/sqlite/README.md +162 -0
  54. package/engines/surrealdb/README.md +218 -0
  55. package/engines/surrealdb/index.ts +1 -2
  56. package/engines/valkey/README.md +219 -0
  57. package/engines/valkey/cli-utils.ts +2 -4
  58. package/package.json +3 -3
  59. package/types/index.ts +25 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/spindb.svg)](https://www.npmjs.com/package/spindb)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/spindb.svg)](https://www.npmjs.com/package/spindb)
5
5
  [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg)](LICENSE)
6
- [![Platform: macOS | Linux | Windows](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#platform-coverage)
6
+ [![Platform: macOS | Linux | Windows](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#supported-engines--platforms)
7
7
 
8
8
  **One CLI for all your local databases.**
9
9
 
@@ -26,6 +26,35 @@ spindb create cache --engine redis
26
26
 
27
27
  ---
28
28
 
29
+ ## Supported Engines & Platforms
30
+
31
+ SpinDB supports **16 database engines** across **5 platform architectures**β€”all with a consistent API.
32
+
33
+ | Engine | Type | macOS ARM | macOS Intel | Linux x64 | Linux ARM | Windows |
34
+ |--------|------|:---------:|:-----------:|:---------:|:---------:|:-------:|
35
+ | 🐘 **PostgreSQL** | Relational SQL | βœ… | βœ… | βœ… | βœ… | βœ… |
36
+ | 🐬 **MySQL** | Relational SQL | βœ… | βœ… | βœ… | βœ… | βœ… |
37
+ | 🦭 **MariaDB** | Relational SQL | βœ… | βœ… | βœ… | βœ… | βœ… |
38
+ | πŸͺΆ **SQLite** | Embedded SQL | βœ… | βœ… | βœ… | βœ… | βœ… |
39
+ | πŸ¦† **DuckDB** | Embedded OLAP | βœ… | βœ… | βœ… | βœ… | βœ… |
40
+ | πŸƒ **MongoDB** | Document Store | βœ… | βœ… | βœ… | βœ… | βœ… |
41
+ | πŸ¦” **FerretDB** | Document Store | βœ… | βœ… | βœ… | βœ… | ❌ |
42
+ | πŸ”΄ **Redis** | Key-Value | βœ… | βœ… | βœ… | βœ… | βœ… |
43
+ | πŸ”· **Valkey** | Key-Value | βœ… | βœ… | βœ… | βœ… | βœ… |
44
+ | 🏠 **ClickHouse** | Columnar OLAP | βœ… | βœ… | βœ… | βœ… | ❌ |
45
+ | 🧭 **Qdrant** | Vector Search | βœ… | βœ… | βœ… | βœ… | βœ… |
46
+ | πŸ” **Meilisearch** | Full-Text Search | βœ… | βœ… | βœ… | βœ… | βœ… |
47
+ | πŸ›‹οΈ **CouchDB** | Document Store | βœ… | βœ… | βœ… | βœ… | βœ… |
48
+ | πŸͺ³ **CockroachDB** | Distributed SQL | βœ… | βœ… | βœ… | βœ… | βœ… |
49
+ | πŸŒ€ **SurrealDB** | Multi-Model | βœ… | βœ… | βœ… | βœ… | βœ… |
50
+ | ⏱️ **QuestDB** | Time-Series | βœ… | βœ… | βœ… | βœ… | βœ… |
51
+
52
+ **78 combinations. One CLI. Zero configuration.**
53
+
54
+ > ClickHouse and FerretDB are available on Windows via WSL.
55
+
56
+ ---
57
+
29
58
  ## What is SpinDB?
30
59
 
31
60
  SpinDB is **three tools in one**:
@@ -68,33 +97,6 @@ spindb run mydb -c "SELECT * FROM system.tables" # ClickHouse
68
97
 
69
98
  ---
70
99
 
71
- ## Platform Coverage
72
-
73
- SpinDB works across **16 database engines** and **5 platform architectures** with a **single, consistent API**.
74
-
75
- | Database | macOS ARM64 | macOS Intel | Linux x64 | Linux ARM64 | Windows x64 |
76
- |----------|:-----------:|:-----------:|:---------:|:-----------:|:-----------:|
77
- | 🐘 **PostgreSQL** | βœ… | βœ… | βœ… | βœ… | βœ… |
78
- | 🐬 **MySQL** | βœ… | βœ… | βœ… | βœ… | βœ… |
79
- | 🦭 **MariaDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
80
- | πŸͺΆ **SQLite** | βœ… | βœ… | βœ… | βœ… | βœ… |
81
- | πŸ¦† **DuckDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
82
- | πŸƒ **MongoDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
83
- | πŸ¦” **FerretDB** | βœ… | βœ… | βœ… | βœ… | ❌ |
84
- | πŸ”΄ **Redis** | βœ… | βœ… | βœ… | βœ… | βœ… |
85
- | πŸ”· **Valkey** | βœ… | βœ… | βœ… | βœ… | βœ… |
86
- | 🏠 **ClickHouse** | βœ… | βœ… | βœ… | βœ… | ❌ |
87
- | 🧭 **Qdrant** | βœ… | βœ… | βœ… | βœ… | βœ… |
88
- | πŸ” **Meilisearch** | βœ… | βœ… | βœ… | βœ… | βœ… |
89
- | πŸ›‹οΈ **CouchDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
90
- | πŸͺ³ **CockroachDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
91
- | πŸŒ€ **SurrealDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
92
- | ⏱️ **QuestDB** | βœ… | βœ… | βœ… | βœ… | βœ… |
93
-
94
- **78 combinations. One CLI. Zero configuration.**
95
-
96
- ---
97
-
98
100
  ## Quick Start
99
101
 
100
102
  Install SpinDB globally using your preferred package manager:
@@ -165,20 +167,56 @@ SpinDB runs databases as **native processes** with **isolated data directories**
165
167
  - **Multi-version support** - Run PostgreSQL 14 and 18 side-by-side
166
168
  - **Unified interface** - Manage PostgreSQL, MongoDB, and Redis the same way
167
169
 
168
- ### Comparison Matrix
169
-
170
- | Feature | SpinDB | Docker | DBngin | Postgres.app | XAMPP |
171
- |---------|--------|--------|--------|--------------|-------|
172
- | **All database types unified** | βœ… 16 engines | ❌ | ❌ | ❌ | ❌ |
173
- | No Docker required | βœ… | ❌ | βœ… | βœ… | βœ… |
174
- | CLI-first | βœ… | βœ… | ❌ GUI-first | ❌ GUI-first | ❌ GUI-first |
175
- | Multiple versions side-by-side | βœ… | βœ… | βœ… | βœ… | ❌ |
176
- | Clone databases | βœ… | Manual | βœ… | ❌ | ❌ |
177
- | Backup/restore built-in | βœ… | Manual | βœ… | ❌ | ❌ |
178
- | Low resource usage | βœ… Native | ❌ VM overhead | βœ… Native | βœ… Native | βœ… Native |
179
- | Linux support | βœ… | βœ… | ❌ | ❌ | βœ… |
180
- | ARM64 support | βœ… | βœ… | βœ… | βœ… | ❌ |
181
- | Free for commercial use | ❌ | ⚠️ Paid for orgs | βœ… | βœ… | βœ… |
170
+ ### Comparison: Database GUI Tools
171
+
172
+ *For developers who prefer visual interfaces or use macOS-native tools.*
173
+
174
+ | Feature | SpinDB | DBngin | Postgres.app | Laragon |
175
+ |---------|--------|--------|--------------|---------|
176
+ | **Engines supported** | 16 | 3 (PG/MySQL/Redis) | 1 (PostgreSQL) | 4 (PG/MySQL/MariaDB/MongoDB) |
177
+ | CLI-first | βœ… | ❌ GUI-only | ❌ GUI-only | ⚠️ Limited CLI |
178
+ | Multi-version support | βœ… | βœ… | βœ… | βœ… |
179
+ | Built-in backup/restore | βœ… | βœ… | ❌ | ⚠️ Manual |
180
+ | Clone databases | βœ… | βœ… | ❌ | ❌ |
181
+ | macOS | βœ… | βœ… | βœ… | ❌ |
182
+ | Linux | βœ… | ❌ | ❌ | ❌ |
183
+ | Windows | βœ… | ❌ | ❌ | βœ… |
184
+ | Free for commercial use | ❌ | βœ… | βœ… | βœ… |
185
+
186
+ ### Comparison: Docker & Containers
187
+
188
+ *For developers already using containerization.*
189
+
190
+ | Feature | SpinDB | Docker Desktop | Podman | OrbStack |
191
+ |---------|--------|----------------|--------|----------|
192
+ | **Engines supported** | 16 unified | Any (manual setup) | Any (manual setup) | Any (manual setup) |
193
+ | Daemon required | ❌ | βœ… | ❌ (rootless) | βœ… |
194
+ | Resource overhead | Native | VM + containers | VM + containers | VM + containers |
195
+ | Built-in backup/restore | βœ… | ❌ Manual | ❌ Manual | ❌ Manual |
196
+ | Connection strings | βœ… Auto-generated | ❌ Manual | ❌ Manual | ❌ Manual |
197
+ | Version switching | βœ… Instant | ⚠️ Pull images | ⚠️ Pull images | ⚠️ Pull images |
198
+ | Database-specific CLI | βœ… Included | ❌ Exec into container | ❌ Exec into container | ❌ Exec into container |
199
+ | Prod parity | ⚠️ Native binaries | βœ… Exact images | βœ… Exact images | βœ… Exact images |
200
+ | Free for commercial use | ❌ | ⚠️ Paid for orgs | βœ… | ⚠️ Paid tiers |
201
+
202
+ ### Comparison: Package Managers
203
+
204
+ *For developers who "just install" databases system-wide.*
205
+
206
+ | Feature | SpinDB | Homebrew | apt/winget | asdf-vm |
207
+ |---------|--------|----------|------------|---------|
208
+ | **Engines supported** | 16 unified | Many (separate formulas) | Many (separate packages) | Many (plugins) |
209
+ | Multi-version side-by-side | βœ… | ⚠️ Complex | ❌ | βœ… |
210
+ | Isolated data directories | βœ… | ❌ System-wide | ❌ System-wide | ❌ |
211
+ | Built-in backup/restore | βœ… | ❌ | ❌ | ❌ |
212
+ | Unified CLI across engines | βœ… | ❌ | ❌ | ❌ |
213
+ | No root/sudo required | βœ… | βœ… | ❌ | βœ… |
214
+ | macOS | βœ… | βœ… | ❌ | βœ… |
215
+ | Linux | βœ… | βœ… | βœ… | βœ… |
216
+ | Windows | βœ… | ❌ | βœ… (winget) | ⚠️ WSL |
217
+ | Free for commercial use | ❌ | βœ… | βœ… | βœ… |
218
+
219
+ > **Note on licensing:** SpinDB requires a commercial license for business use. For personal projects, education, research, nonprofits, and government use, SpinDB is free. See [License](#license) for details.
182
220
 
183
221
  ---
184
222
 
@@ -241,6 +279,7 @@ spindb create mydb --engine mongodb # MongoDB
241
279
  spindb create mydb --engine mysql --db-version 8.0 # MySQL 8.0
242
280
  spindb create mydb --port 5433 # Custom port
243
281
  spindb create mydb --start --connect # Create, start, and connect
282
+ spindb create mydb --force # Overwrite existing container
244
283
 
245
284
  # Start/stop databases
246
285
  spindb start mydb
@@ -290,14 +329,63 @@ spindb backup mydb --format custom # Custom binary format (PostgreS
290
329
  spindb restore mydb backup.dump
291
330
  spindb restore mydb backup.sql --database prod_copy
292
331
 
293
- # Pull from remote database
294
- spindb restore mydb --from-url "postgresql://user:pass@prod-host/db"
295
-
296
332
  # Clone existing database
297
333
  spindb create prod-copy --from ./prod-backup.dump
298
334
  spindb create staging --from "postgresql://user:pass@prod:5432/production"
299
335
  ```
300
336
 
337
+ ### Pull from Remote Database
338
+
339
+ Sync production data to your local database while automatically backing up your original data:
340
+
341
+ ```bash
342
+ # Pull production data (backs up original, replaces with remote)
343
+ spindb pull mydb --from "postgresql://user:pass@prod-host/db"
344
+
345
+ # Read URL from environment variable (keeps credentials out of shell history)
346
+ spindb pull mydb --from-env CLONE_FROM_DATABASE_URL
347
+
348
+ # Clone mode: pull to new database (original untouched)
349
+ spindb pull mydb --from-env PROD_URL --as mydb_prod
350
+
351
+ # Preview what will happen
352
+ spindb pull mydb --from-env PROD_URL --dry-run
353
+
354
+ # Run post-pull script (e.g., sync local credentials)
355
+ spindb pull mydb --from-env PROD_URL --post-script ./sync-credentials.ts
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
+
301
389
  ### Container Management
302
390
 
303
391
  ```bash
@@ -440,8 +528,6 @@ Databases run as **native processes**, and **data persists across restarts**. Wh
440
528
 
441
529
  ## Engine-Specific Details
442
530
 
443
- Each database engine has unique features and behaviors. See full documentation in [ENGINES.md](ENGINES.md).
444
-
445
531
  ### PostgreSQL 🐘
446
532
 
447
533
  ```bash
@@ -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 { path: filePath, from: restoreLocation, connect, json } = options
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 (json) {
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
- while (await containerManager.exists(containerName)) {
80
- console.log(
81
- chalk.yellow(` Container "${containerName}" already exists.`),
82
- )
83
- containerName = await promptContainerName()
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 { path: filePath, from: restoreLocation, connect, json } = options
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 (json) {
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
- while (await containerManager.exists(containerName)) {
233
- console.log(
234
- chalk.yellow(` Container "${containerName}" already exists.`),
235
- )
236
- containerName = await promptContainerName()
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.json) {
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
- while (await containerManager.exists(containerName)) {
743
- console.log(
744
- chalk.yellow(` Container "${containerName}" already exists.`),
745
- )
746
- containerName = await promptContainerName()
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