spindb 0.22.0 → 0.24.0
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 +43 -11
- package/cli/bin.ts +2 -0
- package/cli/commands/create.ts +108 -135
- package/cli/commands/delete.ts +27 -23
- package/cli/commands/doctor.ts +281 -4
- package/cli/commands/engines.ts +180 -1
- package/cli/commands/menu/backup-handlers.ts +181 -167
- package/cli/commands/menu/container-handlers.ts +126 -116
- package/cli/commands/menu/engine-handlers.ts +64 -19
- package/cli/commands/menu/index.ts +66 -26
- package/cli/commands/menu/shared.ts +3 -2
- package/cli/commands/menu/shell-handlers.ts +65 -8
- package/cli/commands/menu/sql-handlers.ts +37 -13
- package/cli/commands/menu/update-handlers.ts +3 -3
- package/cli/commands/start.ts +16 -21
- package/cli/constants.ts +39 -4
- package/cli/helpers.ts +141 -0
- package/cli/ui/prompts.ts +190 -30
- package/config/backup-formats.ts +34 -0
- package/config/engine-defaults.ts +26 -0
- package/config/engines.json +32 -0
- package/core/config-manager.ts +15 -0
- package/core/dependency-manager.ts +4 -0
- package/core/error-handler.ts +81 -0
- package/core/fs-error-utils.ts +6 -3
- package/core/port-manager.ts +5 -1
- package/core/spawn-utils.ts +2 -0
- package/core/test-cleanup.ts +108 -0
- package/core/version-migration.ts +335 -0
- package/engines/couchdb/api-client.ts +81 -0
- package/engines/couchdb/backup.ts +137 -0
- package/engines/couchdb/binary-manager.ts +84 -0
- package/engines/couchdb/binary-urls.ts +115 -0
- package/engines/couchdb/hostdb-releases.ts +23 -0
- package/engines/couchdb/index.ts +1147 -0
- package/engines/couchdb/restore.ts +289 -0
- package/engines/couchdb/version-maps.ts +78 -0
- package/engines/couchdb/version-validator.ts +111 -0
- package/engines/ferretdb/backup.ts +165 -0
- package/engines/ferretdb/binary-manager.ts +897 -0
- package/engines/ferretdb/binary-urls.ts +138 -0
- package/engines/ferretdb/index.ts +1108 -0
- package/engines/ferretdb/restore.ts +345 -0
- package/engines/ferretdb/version-maps.ts +124 -0
- package/engines/index.ts +34 -12
- package/engines/mongodb/hostdb-releases.ts +0 -1
- package/engines/mongodb/index.ts +10 -13
- package/package.json +2 -1
- package/types/index.ts +22 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
**One CLI for all your local databases.**
|
|
9
9
|
|
|
10
|
-
SpinDB is a universal database management tool that combines a package manager, a unified API, and native client tooling for
|
|
10
|
+
SpinDB is a universal database management tool that combines a package manager, a unified API, and native client tooling for 13 different database engines—all from a single command-line interface. No Docker, no VMs, no platform-specific installers. Just databases, running natively on your machine.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
npm install -g spindb
|
|
@@ -48,7 +48,7 @@ One consistent interface across SQL databases, document stores, key-value stores
|
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
# Same commands work for ANY database
|
|
51
|
-
spindb create mydb --engine [postgresql|mysql|mariadb|mongodb|redis|valkey|clickhouse|sqlite|duckdb|qdrant|meilisearch]
|
|
51
|
+
spindb create mydb --engine [postgresql|mysql|mariadb|mongodb|ferretdb|redis|valkey|clickhouse|sqlite|duckdb|qdrant|meilisearch|couchdb]
|
|
52
52
|
spindb start mydb
|
|
53
53
|
spindb connect mydb
|
|
54
54
|
spindb backup mydb
|
|
@@ -70,7 +70,7 @@ spindb run mydb -c "SELECT * FROM system.tables" # ClickHouse
|
|
|
70
70
|
|
|
71
71
|
## Platform Coverage
|
|
72
72
|
|
|
73
|
-
SpinDB works across **
|
|
73
|
+
SpinDB works across **13 database engines** and **5 platform architectures** with a **single, consistent API**.
|
|
74
74
|
|
|
75
75
|
| Database | macOS ARM64 | macOS Intel | Linux x64 | Linux ARM64 | Windows x64 |
|
|
76
76
|
|----------|:-----------:|:-----------:|:---------:|:-----------:|:-----------:|
|
|
@@ -80,13 +80,15 @@ SpinDB works across **11 database engines** and **5 platform architectures** wit
|
|
|
80
80
|
| 🪶 **SQLite** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
81
81
|
| 🦆 **DuckDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
82
82
|
| 🍃 **MongoDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
83
|
+
| 🦔 **FerretDB** | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
83
84
|
| 🔴 **Redis** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
84
85
|
| 🔷 **Valkey** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
85
86
|
| 🏠 **ClickHouse** | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
86
87
|
| 🧭 **Qdrant** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
87
88
|
| 🔍 **Meilisearch** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
89
|
+
| 🛋 **CouchDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
88
90
|
|
|
89
|
-
**
|
|
91
|
+
**63 combinations. One CLI. Zero configuration.**
|
|
90
92
|
|
|
91
93
|
---
|
|
92
94
|
|
|
@@ -165,7 +167,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
|
|
|
165
167
|
| Feature | SpinDB | Docker | DBngin | Postgres.app | XAMPP |
|
|
166
168
|
|---------|--------|--------|--------|--------------|-------|
|
|
167
169
|
| No Docker required | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
168
|
-
| Multiple DB engines | ✅
|
|
170
|
+
| Multiple DB engines | ✅ 13 engines | ✅ Unlimited | ✅ 3 engines | ❌ PostgreSQL only | ⚠️ MySQL only |
|
|
169
171
|
| CLI-first | ✅ | ✅ | ❌ GUI-first | ❌ GUI-first | ❌ GUI-first |
|
|
170
172
|
| Multiple versions | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
171
173
|
| Clone databases | ✅ | Manual | ✅ | ❌ | ❌ |
|
|
@@ -179,7 +181,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
|
|
|
179
181
|
|
|
180
182
|
## Supported Databases
|
|
181
183
|
|
|
182
|
-
SpinDB supports **
|
|
184
|
+
SpinDB supports **13 database engines** with **multiple versions** for each:
|
|
183
185
|
|
|
184
186
|
| Engine | Type | Versions | Default Port | Query Language |
|
|
185
187
|
|--------|------|----------|--------------|----------------|
|
|
@@ -189,15 +191,17 @@ SpinDB supports **11 database engines** with **multiple versions** for each:
|
|
|
189
191
|
| 🪶 **SQLite** | Embedded (SQL) | 3 | N/A (file-based) | SQL |
|
|
190
192
|
| 🦆 **DuckDB** | Embedded OLAP | 1.4.3 | N/A (file-based) | SQL |
|
|
191
193
|
| 🍃 **MongoDB** | Document Store | 7.0, 8.0, 8.2 | 27017 | JavaScript (mongosh) |
|
|
194
|
+
| 🦔 **FerretDB** | Document Store | 2 | 27017 | JavaScript (mongosh) |
|
|
192
195
|
| 🔴 **Redis** | Key-Value Store | 7, 8 | 6379 | Redis commands |
|
|
193
196
|
| 🔷 **Valkey** | Key-Value Store | 8, 9 | 6379 | Redis commands |
|
|
194
197
|
| 🏠 **ClickHouse** | Columnar OLAP | 25.12 | 9000 (TCP), 8123 (HTTP) | SQL (ClickHouse dialect) |
|
|
195
198
|
| 🧭 **Qdrant** | Vector Search | 1 | 6333 (HTTP), 6334 (gRPC) | REST API |
|
|
196
199
|
| 🔍 **Meilisearch** | Full-Text Search | 1 | 7700 | REST API |
|
|
200
|
+
| 🛋 **CouchDB** | Document Store | 3 | 5984 | REST API |
|
|
197
201
|
|
|
198
202
|
### Engine Categories
|
|
199
203
|
|
|
200
|
-
**Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch):
|
|
204
|
+
**Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, FerretDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch, CouchDB):
|
|
201
205
|
- Start/stop server processes
|
|
202
206
|
- Bind to localhost ports
|
|
203
207
|
- Data stored in `~/.spindb/containers/{engine}/{name}/`
|
|
@@ -337,8 +341,10 @@ spindb config show # Show current config
|
|
|
337
341
|
spindb config detect # Re-detect tool paths
|
|
338
342
|
spindb config update-check on # Enable update notifications
|
|
339
343
|
|
|
340
|
-
#
|
|
344
|
+
# Doctor
|
|
341
345
|
spindb doctor # Interactive health check
|
|
346
|
+
spindb doctor --fix # Auto-fix all issues
|
|
347
|
+
spindb doctor --dry-run # Preview fixes without applying
|
|
342
348
|
spindb doctor --json # JSON output
|
|
343
349
|
|
|
344
350
|
# Version management
|
|
@@ -481,6 +487,29 @@ spindb connect logs
|
|
|
481
487
|
**Query language:** JavaScript (via `mongosh`)
|
|
482
488
|
**Tools:** `mongod`, `mongosh`, `mongodump`, `mongorestore` (included)
|
|
483
489
|
|
|
490
|
+
### FerretDB 🦔
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
# Create FerretDB database (MongoDB-compatible, PostgreSQL backend)
|
|
494
|
+
spindb create docs --engine ferretdb
|
|
495
|
+
|
|
496
|
+
# Same MongoDB queries work
|
|
497
|
+
spindb run docs -c "db.users.insertOne({name: 'Alice'})"
|
|
498
|
+
spindb run docs -c "db.users.find().pretty()"
|
|
499
|
+
|
|
500
|
+
# Connect with mongosh
|
|
501
|
+
spindb connect docs
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Version:** 2 (2.7.0)
|
|
505
|
+
**Platforms:** macOS, Linux (no Windows support)
|
|
506
|
+
**Architecture:** FerretDB proxy + PostgreSQL with DocumentDB extension
|
|
507
|
+
**Query language:** JavaScript (via `mongosh`)
|
|
508
|
+
**Backups:** Uses `pg_dump` on embedded PostgreSQL backend
|
|
509
|
+
**Tools:** `ferretdb`, `mongosh` (for client connections), `pg_dump`/`pg_restore` (bundled with embedded PostgreSQL)
|
|
510
|
+
|
|
511
|
+
FerretDB is a MongoDB-compatible database that stores data in PostgreSQL. It's useful when you want MongoDB's API but PostgreSQL's reliability and SQL access to your data.
|
|
512
|
+
|
|
484
513
|
### Redis 🔴 & Valkey 🔷
|
|
485
514
|
|
|
486
515
|
```bash
|
|
@@ -568,11 +597,13 @@ SpinDB supports enhanced database shells with auto-completion, syntax highlighti
|
|
|
568
597
|
| SQLite | `sqlite3` | `litecli` | `usql` |
|
|
569
598
|
| DuckDB | `duckdb` | - | `usql` |
|
|
570
599
|
| MongoDB | `mongosh` | - | - |
|
|
600
|
+
| FerretDB | `mongosh` | - | - |
|
|
571
601
|
| Redis | `redis-cli` | `iredis` | - |
|
|
572
602
|
| Valkey | `valkey-cli` | `iredis` (compatible) | - |
|
|
573
603
|
| ClickHouse | `clickhouse-client` | - | `usql` |
|
|
574
604
|
| Qdrant | REST API | - | - |
|
|
575
605
|
| Meilisearch | REST API | - | - |
|
|
606
|
+
| CouchDB | REST API | - | - |
|
|
576
607
|
|
|
577
608
|
Install and use in one command:
|
|
578
609
|
|
|
@@ -723,6 +754,7 @@ spindb restore mydb --from-url "postgresql://user:pass@prod-host:5432/production
|
|
|
723
754
|
| ClickHouse | `clickhouse://` or `http://` | `clickhouse://default:pass@host:8123/db` |
|
|
724
755
|
| Qdrant | `qdrant://` or `http://` | `http://host:6333?api_key=KEY` |
|
|
725
756
|
| Meilisearch | `meilisearch://` or `http://` | `http://host:7700?api_key=KEY` |
|
|
757
|
+
| CouchDB | `couchdb://` or `http://` | `http://user:pass@host:5984/db` |
|
|
726
758
|
|
|
727
759
|
### Multi-Version Support
|
|
728
760
|
|
|
@@ -779,7 +811,6 @@ See [TODO.md](TODO.md) for the complete roadmap.
|
|
|
779
811
|
|
|
780
812
|
### v1.2 - Additional Engines
|
|
781
813
|
- **CockroachDB** - Distributed PostgreSQL-compatible database
|
|
782
|
-
- **FerretDB** - MongoDB-compatible database built on PostgreSQL
|
|
783
814
|
|
|
784
815
|
### v1.3 - Advanced Features
|
|
785
816
|
- Container templates for common configurations
|
|
@@ -803,7 +834,8 @@ The following engines may be added based on community interest:
|
|
|
803
834
|
|
|
804
835
|
- **Local only** - Databases bind to `127.0.0.1`. Remote connection support planned for v1.1.
|
|
805
836
|
- **ClickHouse Windows** - Not supported (hostdb doesn't build for Windows).
|
|
806
|
-
- **
|
|
837
|
+
- **FerretDB Windows** - Not supported (postgresql-documentdb has startup issues on Windows).
|
|
838
|
+
- **Qdrant, Meilisearch & CouchDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
|
|
807
839
|
|
|
808
840
|
---
|
|
809
841
|
|
|
@@ -878,7 +910,7 @@ See [FEATURE.md](FEATURE.md) for adding new database engines.
|
|
|
878
910
|
|
|
879
911
|
SpinDB is powered by:
|
|
880
912
|
|
|
881
|
-
- **[hostdb](https://github.com/robertjbass/hostdb)** - Pre-compiled database binaries for
|
|
913
|
+
- **[hostdb](https://github.com/robertjbass/hostdb)** - Pre-compiled database binaries for 14 engines across all major platforms. Makes Docker-free multi-version database support possible.
|
|
882
914
|
|
|
883
915
|
---
|
|
884
916
|
|
package/cli/bin.ts
CHANGED
package/cli/commands/create.ts
CHANGED
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
promptConfirm,
|
|
14
14
|
} from '../ui/prompts'
|
|
15
15
|
import { createSpinner } from '../ui/spinner'
|
|
16
|
-
import { header,
|
|
16
|
+
import { header, connectionBox } from '../ui/theme'
|
|
17
17
|
import { tmpdir } from 'os'
|
|
18
18
|
import { join } from 'path'
|
|
19
19
|
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
|
+
import { isValidDatabaseName, exitWithError } from '../../core/error-handler'
|
|
24
24
|
import { resolve } from 'path'
|
|
25
25
|
import { Engine } from '../../types'
|
|
26
26
|
import type { BaseEngine } from '../../engines/base-engine'
|
|
@@ -49,8 +49,10 @@ async function createSqliteContainer(
|
|
|
49
49
|
const missingDeps = await getMissingDependencies('sqlite')
|
|
50
50
|
if (missingDeps.length > 0) {
|
|
51
51
|
if (json) {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
return exitWithError({
|
|
53
|
+
message: `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
54
|
+
json: true,
|
|
55
|
+
})
|
|
54
56
|
}
|
|
55
57
|
depsSpinner?.warn(
|
|
56
58
|
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
@@ -60,7 +62,7 @@ async function createSqliteContainer(
|
|
|
60
62
|
'sqlite',
|
|
61
63
|
)
|
|
62
64
|
if (!installed) {
|
|
63
|
-
|
|
65
|
+
return exitWithError({ message: 'Required tools not installed' })
|
|
64
66
|
}
|
|
65
67
|
} else {
|
|
66
68
|
depsSpinner?.succeed('Required tools available')
|
|
@@ -69,8 +71,10 @@ async function createSqliteContainer(
|
|
|
69
71
|
// Check if container already exists
|
|
70
72
|
if (await containerManager.exists(containerName)) {
|
|
71
73
|
if (json) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
return exitWithError({
|
|
75
|
+
message: `Container "${containerName}" already exists`,
|
|
76
|
+
json: true,
|
|
77
|
+
})
|
|
74
78
|
}
|
|
75
79
|
while (await containerManager.exists(containerName)) {
|
|
76
80
|
console.log(chalk.yellow(` Container "${containerName}" already exists.`))
|
|
@@ -84,12 +88,10 @@ async function createSqliteContainer(
|
|
|
84
88
|
|
|
85
89
|
// Check if file already exists
|
|
86
90
|
if (existsSync(absolutePath)) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
process.exit(1)
|
|
91
|
+
return exitWithError({
|
|
92
|
+
message: `File already exists: ${absolutePath}`,
|
|
93
|
+
json,
|
|
94
|
+
})
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
const createSpinnerInstance = json ? null : createSpinner('Creating SQLite database...')
|
|
@@ -196,8 +198,10 @@ async function createDuckDBContainer(
|
|
|
196
198
|
const missingDeps = await getMissingDependencies('duckdb')
|
|
197
199
|
if (missingDeps.length > 0) {
|
|
198
200
|
if (json) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
return exitWithError({
|
|
202
|
+
message: `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
203
|
+
json: true,
|
|
204
|
+
})
|
|
201
205
|
}
|
|
202
206
|
depsSpinner?.warn(
|
|
203
207
|
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
@@ -207,7 +211,7 @@ async function createDuckDBContainer(
|
|
|
207
211
|
'duckdb',
|
|
208
212
|
)
|
|
209
213
|
if (!installed) {
|
|
210
|
-
|
|
214
|
+
return exitWithError({ message: 'Required tools not installed' })
|
|
211
215
|
}
|
|
212
216
|
} else {
|
|
213
217
|
depsSpinner?.succeed('Required tools available')
|
|
@@ -216,8 +220,10 @@ async function createDuckDBContainer(
|
|
|
216
220
|
// Check if container already exists
|
|
217
221
|
if (await containerManager.exists(containerName)) {
|
|
218
222
|
if (json) {
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
return exitWithError({
|
|
224
|
+
message: `Container "${containerName}" already exists`,
|
|
225
|
+
json: true,
|
|
226
|
+
})
|
|
221
227
|
}
|
|
222
228
|
while (await containerManager.exists(containerName)) {
|
|
223
229
|
console.log(chalk.yellow(` Container "${containerName}" already exists.`))
|
|
@@ -231,12 +237,10 @@ async function createDuckDBContainer(
|
|
|
231
237
|
|
|
232
238
|
// Check if file already exists
|
|
233
239
|
if (existsSync(absolutePath)) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
process.exit(1)
|
|
240
|
+
return exitWithError({
|
|
241
|
+
message: `File already exists: ${absolutePath}`,
|
|
242
|
+
json,
|
|
243
|
+
})
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
const createSpinnerInstance = json ? null : createSpinner('Creating DuckDB database...')
|
|
@@ -379,7 +383,7 @@ export const createCommand = new Command('create')
|
|
|
379
383
|
.argument('[name]', 'Container name')
|
|
380
384
|
.option(
|
|
381
385
|
'-e, --engine <engine>',
|
|
382
|
-
'Database engine (postgresql, mysql, mariadb, sqlite, duckdb, mongodb, redis, valkey, clickhouse, qdrant, meilisearch)',
|
|
386
|
+
'Database engine (postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch)',
|
|
383
387
|
)
|
|
384
388
|
.option('--db-version <version>', 'Database version (e.g., 17, 8.0)')
|
|
385
389
|
.option('-d, --database <database>', 'Database name')
|
|
@@ -431,22 +435,10 @@ export const createCommand = new Command('create')
|
|
|
431
435
|
const locationInfo = detectLocationType(options.from)
|
|
432
436
|
|
|
433
437
|
if (locationInfo.type === 'not_found') {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
console.log(
|
|
439
|
-
chalk.gray(
|
|
440
|
-
' Provide a valid file path or connection string:',
|
|
441
|
-
),
|
|
442
|
-
)
|
|
443
|
-
console.log(
|
|
444
|
-
chalk.gray(
|
|
445
|
-
' postgresql://, mysql://, mongodb://, redis://, sqlite://, duckdb://',
|
|
446
|
-
),
|
|
447
|
-
)
|
|
448
|
-
}
|
|
449
|
-
process.exit(1)
|
|
438
|
+
return exitWithError({
|
|
439
|
+
message: `Location not found: ${options.from}. Provide a valid file path or connection string (postgresql://, mysql://, redis://, sqlite://, duckdb://)`,
|
|
440
|
+
json: options.json,
|
|
441
|
+
})
|
|
450
442
|
}
|
|
451
443
|
|
|
452
444
|
restoreLocation = options.from
|
|
@@ -464,16 +456,10 @@ export const createCommand = new Command('create')
|
|
|
464
456
|
}
|
|
465
457
|
|
|
466
458
|
if (options.start === false) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
uiError(
|
|
472
|
-
'Cannot use --no-start with --from (restore requires running container)',
|
|
473
|
-
),
|
|
474
|
-
)
|
|
475
|
-
}
|
|
476
|
-
process.exit(1)
|
|
459
|
+
return exitWithError({
|
|
460
|
+
message: 'Cannot use --no-start with --from (restore requires running container)',
|
|
461
|
+
json: options.json,
|
|
462
|
+
})
|
|
477
463
|
}
|
|
478
464
|
}
|
|
479
465
|
|
|
@@ -486,8 +472,7 @@ export const createCommand = new Command('create')
|
|
|
486
472
|
if (!containerName) {
|
|
487
473
|
// JSON mode requires container name argument
|
|
488
474
|
if (options.json) {
|
|
489
|
-
|
|
490
|
-
process.exit(1)
|
|
475
|
+
return exitWithError({ message: 'Container name is required', json: true })
|
|
491
476
|
}
|
|
492
477
|
|
|
493
478
|
const answers = await promptCreateOptions()
|
|
@@ -504,35 +489,26 @@ export const createCommand = new Command('create')
|
|
|
504
489
|
// Validate Redis/Valkey database is a pure integer string 0-15
|
|
505
490
|
// Reject decimals ("1.5"), scientific notation ("1e2"), and trailing garbage ("5abc")
|
|
506
491
|
if (!/^[0-9]+$/.test(database)) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
console.error(uiError(errorMsg))
|
|
512
|
-
}
|
|
513
|
-
process.exit(1)
|
|
492
|
+
return exitWithError({
|
|
493
|
+
message: 'Redis/Valkey database must be an integer between 0 and 15',
|
|
494
|
+
json: options.json,
|
|
495
|
+
})
|
|
514
496
|
}
|
|
515
497
|
const dbIndex = parseInt(database, 10)
|
|
516
498
|
if (dbIndex < 0 || dbIndex > 15) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
console.error(uiError(errorMsg))
|
|
522
|
-
}
|
|
523
|
-
process.exit(1)
|
|
499
|
+
return exitWithError({
|
|
500
|
+
message: 'Redis/Valkey database must be an integer between 0 and 15',
|
|
501
|
+
json: options.json,
|
|
502
|
+
})
|
|
524
503
|
}
|
|
525
504
|
} else {
|
|
526
505
|
database = database ?? containerName.replace(/-/g, '_')
|
|
527
506
|
// Validate database name to prevent SQL injection
|
|
528
507
|
if (!isValidDatabaseName(database)) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
console.error(uiError(errorMsg))
|
|
534
|
-
}
|
|
535
|
-
process.exit(1)
|
|
508
|
+
return exitWithError({
|
|
509
|
+
message: 'Database name must start with a letter and contain only letters, numbers, and underscores',
|
|
510
|
+
json: options.json,
|
|
511
|
+
})
|
|
536
512
|
}
|
|
537
513
|
}
|
|
538
514
|
|
|
@@ -568,33 +544,28 @@ export const createCommand = new Command('create')
|
|
|
568
544
|
|
|
569
545
|
// For server databases, validate --connect with --no-start
|
|
570
546
|
if (options.connect && options.start === false) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
console.error(uiError(errorMsg))
|
|
576
|
-
}
|
|
577
|
-
process.exit(1)
|
|
547
|
+
return exitWithError({
|
|
548
|
+
message: 'Cannot use --no-start with --connect (connection requires running container)',
|
|
549
|
+
json: options.json,
|
|
550
|
+
})
|
|
578
551
|
}
|
|
579
552
|
|
|
580
553
|
// In JSON mode, require explicit --start or --no-start flag to avoid interactive prompts
|
|
581
554
|
if (options.json && options.start === undefined && !restoreLocation && !options.connect) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
555
|
+
return exitWithError({
|
|
556
|
+
message: 'In JSON mode, you must specify --start or --no-start for server databases',
|
|
557
|
+
json: true,
|
|
558
|
+
})
|
|
585
559
|
}
|
|
586
560
|
|
|
587
561
|
// Validate --max-connections if provided
|
|
588
562
|
if (options.maxConnections) {
|
|
589
563
|
const parsed = parseInt(options.maxConnections, 10)
|
|
590
564
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
console.error(uiError(errorMsg))
|
|
596
|
-
}
|
|
597
|
-
process.exit(1)
|
|
565
|
+
return exitWithError({
|
|
566
|
+
message: 'Invalid --max-connections value: must be a positive integer',
|
|
567
|
+
json: options.json,
|
|
568
|
+
})
|
|
598
569
|
}
|
|
599
570
|
}
|
|
600
571
|
|
|
@@ -606,12 +577,11 @@ export const createCommand = new Command('create')
|
|
|
606
577
|
port = parseInt(options.port, 10)
|
|
607
578
|
const available = await portManager.isPortAvailable(port)
|
|
608
579
|
if (!available) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
process.exit(1)
|
|
580
|
+
portSpinner?.fail(`Port ${port} is already in use`)
|
|
581
|
+
return exitWithError({
|
|
582
|
+
message: `Port ${port} is already in use`,
|
|
583
|
+
json: options.json,
|
|
584
|
+
})
|
|
615
585
|
}
|
|
616
586
|
portSpinner?.succeed(`Using port ${port}`)
|
|
617
587
|
} else {
|
|
@@ -661,10 +631,10 @@ export const createCommand = new Command('create')
|
|
|
661
631
|
if (missingDeps.length > 0) {
|
|
662
632
|
// In JSON mode, error out instead of prompting
|
|
663
633
|
if (options.json) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
634
|
+
return exitWithError({
|
|
635
|
+
message: `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
636
|
+
json: true,
|
|
637
|
+
})
|
|
668
638
|
}
|
|
669
639
|
|
|
670
640
|
depsSpinner?.warn(
|
|
@@ -677,17 +647,14 @@ export const createCommand = new Command('create')
|
|
|
677
647
|
)
|
|
678
648
|
|
|
679
649
|
if (!installed) {
|
|
680
|
-
|
|
650
|
+
return exitWithError({ message: 'Required tools not installed' })
|
|
681
651
|
}
|
|
682
652
|
|
|
683
653
|
missingDeps = await getMissingDependencies(engine)
|
|
684
654
|
if (missingDeps.length > 0) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
),
|
|
689
|
-
)
|
|
690
|
-
process.exit(1)
|
|
655
|
+
return exitWithError({
|
|
656
|
+
message: `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
657
|
+
})
|
|
691
658
|
}
|
|
692
659
|
|
|
693
660
|
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
@@ -717,23 +684,23 @@ export const createCommand = new Command('create')
|
|
|
717
684
|
)
|
|
718
685
|
binarySpinner?.succeed(`${dbEngine.displayName} ${version} binaries ready`)
|
|
719
686
|
} catch (error) {
|
|
687
|
+
binarySpinner?.fail(`${dbEngine.displayName} ${version} not available`)
|
|
720
688
|
if (options.json) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
689
|
+
return exitWithError({
|
|
690
|
+
message: `${dbEngine.displayName} ${version} not available`,
|
|
691
|
+
json: true,
|
|
692
|
+
})
|
|
725
693
|
}
|
|
726
|
-
binarySpinner?.fail(`${dbEngine.displayName} ${version} not available`)
|
|
727
694
|
throw error
|
|
728
695
|
}
|
|
729
696
|
}
|
|
730
697
|
|
|
731
698
|
if (await containerManager.exists(containerName)) {
|
|
732
699
|
if (options.json) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
700
|
+
return exitWithError({
|
|
701
|
+
message: `Container "${containerName}" already exists`,
|
|
702
|
+
json: true,
|
|
703
|
+
})
|
|
737
704
|
}
|
|
738
705
|
while (await containerManager.exists(containerName)) {
|
|
739
706
|
console.log(
|
|
@@ -798,8 +765,14 @@ export const createCommand = new Command('create')
|
|
|
798
765
|
} else if (options.start === false) {
|
|
799
766
|
shouldStart = false
|
|
800
767
|
} else {
|
|
801
|
-
|
|
802
|
-
|
|
768
|
+
// In non-interactive mode (no TTY), default to not starting
|
|
769
|
+
// This allows scripts/CI to run without --no-start flag
|
|
770
|
+
if (!process.stdin.isTTY) {
|
|
771
|
+
shouldStart = false
|
|
772
|
+
} else {
|
|
773
|
+
console.log()
|
|
774
|
+
shouldStart = await promptConfirm(`Start ${containerName} now?`, true)
|
|
775
|
+
}
|
|
803
776
|
}
|
|
804
777
|
|
|
805
778
|
const config = await containerManager.getConfig(containerName)
|
|
@@ -918,23 +891,29 @@ export const createCommand = new Command('create')
|
|
|
918
891
|
e.message.includes('pg_dump not found') ||
|
|
919
892
|
e.message.includes('ENOENT')
|
|
920
893
|
) {
|
|
894
|
+
// In JSON mode, don't prompt - just exit with error
|
|
895
|
+
if (options.json) {
|
|
896
|
+
return exitWithError({ message: 'pg_dump not installed', json: true })
|
|
897
|
+
}
|
|
921
898
|
const installed = await promptInstallDependencies('pg_dump')
|
|
922
899
|
if (!installed) {
|
|
923
|
-
|
|
900
|
+
return exitWithError({ message: 'pg_dump not installed', json: options.json })
|
|
924
901
|
}
|
|
925
902
|
continue
|
|
926
903
|
}
|
|
927
904
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
905
|
+
return exitWithError({
|
|
906
|
+
message: `pg_dump error: ${e.message}`,
|
|
907
|
+
json: options.json,
|
|
908
|
+
})
|
|
932
909
|
}
|
|
933
910
|
}
|
|
934
911
|
|
|
935
912
|
if (!dumpSuccess) {
|
|
936
|
-
|
|
937
|
-
|
|
913
|
+
return exitWithError({
|
|
914
|
+
message: 'Failed to create dump after retries',
|
|
915
|
+
json: options.json,
|
|
916
|
+
})
|
|
938
917
|
}
|
|
939
918
|
} else {
|
|
940
919
|
backupPath = restoreLocation
|
|
@@ -1049,8 +1028,7 @@ export const createCommand = new Command('create')
|
|
|
1049
1028
|
|
|
1050
1029
|
if (matchingPattern) {
|
|
1051
1030
|
if (options.json) {
|
|
1052
|
-
|
|
1053
|
-
process.exit(1)
|
|
1031
|
+
return exitWithError({ message: e.message, json: true })
|
|
1054
1032
|
}
|
|
1055
1033
|
const missingTool = matchingPattern.replace(' not found', '')
|
|
1056
1034
|
const installed = await promptInstallDependencies(missingTool)
|
|
@@ -1059,15 +1037,10 @@ export const createCommand = new Command('create')
|
|
|
1059
1037
|
chalk.yellow(' Please re-run your command to continue.'),
|
|
1060
1038
|
)
|
|
1061
1039
|
}
|
|
1062
|
-
|
|
1040
|
+
return exitWithError({ message: 'Missing required tools', json: options.json })
|
|
1063
1041
|
}
|
|
1064
1042
|
|
|
1065
|
-
|
|
1066
|
-
console.log(JSON.stringify({ error: e.message }))
|
|
1067
|
-
} else {
|
|
1068
|
-
console.error(uiError(e.message))
|
|
1069
|
-
}
|
|
1070
|
-
process.exit(1)
|
|
1043
|
+
return exitWithError({ message: e.message, json: options.json })
|
|
1071
1044
|
} finally {
|
|
1072
1045
|
if (tempDumpPath) {
|
|
1073
1046
|
try {
|