spindb 0.22.0 → 0.23.5
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 +37 -9
- 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 +105 -1
- package/cli/commands/menu/backup-handlers.ts +172 -167
- package/cli/commands/menu/container-handlers.ts +74 -84
- 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 +8 -6
- package/cli/commands/menu/sql-handlers.ts +36 -13
- package/cli/commands/menu/update-handlers.ts +3 -3
- package/cli/commands/start.ts +16 -21
- package/cli/constants.ts +37 -4
- package/cli/helpers.ts +73 -0
- package/cli/ui/prompts.ts +187 -28
- package/config/backup-formats.ts +20 -0
- package/config/engine-defaults.ts +13 -0
- package/config/engines.json +16 -0
- package/core/config-manager.ts +10 -0
- package/core/dependency-manager.ts +2 -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/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 +30 -12
- package/engines/mongodb/hostdb-releases.ts +0 -1
- package/engines/mongodb/index.ts +10 -13
- package/package.json +1 -1
- package/types/index.ts +13 -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 12 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]
|
|
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 **12 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,14 @@ 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** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
88
89
|
|
|
89
|
-
**
|
|
90
|
+
**58 combinations. One CLI. Zero configuration.**
|
|
90
91
|
|
|
91
92
|
---
|
|
92
93
|
|
|
@@ -165,7 +166,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
|
|
|
165
166
|
| Feature | SpinDB | Docker | DBngin | Postgres.app | XAMPP |
|
|
166
167
|
|---------|--------|--------|--------|--------------|-------|
|
|
167
168
|
| No Docker required | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
168
|
-
| Multiple DB engines | ✅
|
|
169
|
+
| Multiple DB engines | ✅ 12 engines | ✅ Unlimited | ✅ 3 engines | ❌ PostgreSQL only | ⚠️ MySQL only |
|
|
169
170
|
| CLI-first | ✅ | ✅ | ❌ GUI-first | ❌ GUI-first | ❌ GUI-first |
|
|
170
171
|
| Multiple versions | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
171
172
|
| Clone databases | ✅ | Manual | ✅ | ❌ | ❌ |
|
|
@@ -179,7 +180,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
|
|
|
179
180
|
|
|
180
181
|
## Supported Databases
|
|
181
182
|
|
|
182
|
-
SpinDB supports **
|
|
183
|
+
SpinDB supports **12 database engines** with **multiple versions** for each:
|
|
183
184
|
|
|
184
185
|
| Engine | Type | Versions | Default Port | Query Language |
|
|
185
186
|
|--------|------|----------|--------------|----------------|
|
|
@@ -189,6 +190,7 @@ SpinDB supports **11 database engines** with **multiple versions** for each:
|
|
|
189
190
|
| 🪶 **SQLite** | Embedded (SQL) | 3 | N/A (file-based) | SQL |
|
|
190
191
|
| 🦆 **DuckDB** | Embedded OLAP | 1.4.3 | N/A (file-based) | SQL |
|
|
191
192
|
| 🍃 **MongoDB** | Document Store | 7.0, 8.0, 8.2 | 27017 | JavaScript (mongosh) |
|
|
193
|
+
| 🦔 **FerretDB** | Document Store | 2 | 27017 | JavaScript (mongosh) |
|
|
192
194
|
| 🔴 **Redis** | Key-Value Store | 7, 8 | 6379 | Redis commands |
|
|
193
195
|
| 🔷 **Valkey** | Key-Value Store | 8, 9 | 6379 | Redis commands |
|
|
194
196
|
| 🏠 **ClickHouse** | Columnar OLAP | 25.12 | 9000 (TCP), 8123 (HTTP) | SQL (ClickHouse dialect) |
|
|
@@ -197,7 +199,7 @@ SpinDB supports **11 database engines** with **multiple versions** for each:
|
|
|
197
199
|
|
|
198
200
|
### Engine Categories
|
|
199
201
|
|
|
200
|
-
**Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch):
|
|
202
|
+
**Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, FerretDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch):
|
|
201
203
|
- Start/stop server processes
|
|
202
204
|
- Bind to localhost ports
|
|
203
205
|
- Data stored in `~/.spindb/containers/{engine}/{name}/`
|
|
@@ -337,8 +339,10 @@ spindb config show # Show current config
|
|
|
337
339
|
spindb config detect # Re-detect tool paths
|
|
338
340
|
spindb config update-check on # Enable update notifications
|
|
339
341
|
|
|
340
|
-
#
|
|
342
|
+
# Doctor
|
|
341
343
|
spindb doctor # Interactive health check
|
|
344
|
+
spindb doctor --fix # Auto-fix all issues
|
|
345
|
+
spindb doctor --dry-run # Preview fixes without applying
|
|
342
346
|
spindb doctor --json # JSON output
|
|
343
347
|
|
|
344
348
|
# Version management
|
|
@@ -481,6 +485,29 @@ spindb connect logs
|
|
|
481
485
|
**Query language:** JavaScript (via `mongosh`)
|
|
482
486
|
**Tools:** `mongod`, `mongosh`, `mongodump`, `mongorestore` (included)
|
|
483
487
|
|
|
488
|
+
### FerretDB 🦔
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
# Create FerretDB database (MongoDB-compatible, PostgreSQL backend)
|
|
492
|
+
spindb create docs --engine ferretdb
|
|
493
|
+
|
|
494
|
+
# Same MongoDB queries work
|
|
495
|
+
spindb run docs -c "db.users.insertOne({name: 'Alice'})"
|
|
496
|
+
spindb run docs -c "db.users.find().pretty()"
|
|
497
|
+
|
|
498
|
+
# Connect with mongosh
|
|
499
|
+
spindb connect docs
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Version:** 2 (2.7.0)
|
|
503
|
+
**Platforms:** macOS, Linux (no Windows support)
|
|
504
|
+
**Architecture:** FerretDB proxy + PostgreSQL with DocumentDB extension
|
|
505
|
+
**Query language:** JavaScript (via `mongosh`)
|
|
506
|
+
**Backups:** Uses `pg_dump` on embedded PostgreSQL backend
|
|
507
|
+
**Tools:** `ferretdb`, `mongosh` (for client connections), `pg_dump`/`pg_restore` (bundled with embedded PostgreSQL)
|
|
508
|
+
|
|
509
|
+
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.
|
|
510
|
+
|
|
484
511
|
### Redis 🔴 & Valkey 🔷
|
|
485
512
|
|
|
486
513
|
```bash
|
|
@@ -568,6 +595,7 @@ SpinDB supports enhanced database shells with auto-completion, syntax highlighti
|
|
|
568
595
|
| SQLite | `sqlite3` | `litecli` | `usql` |
|
|
569
596
|
| DuckDB | `duckdb` | - | `usql` |
|
|
570
597
|
| MongoDB | `mongosh` | - | - |
|
|
598
|
+
| FerretDB | `mongosh` | - | - |
|
|
571
599
|
| Redis | `redis-cli` | `iredis` | - |
|
|
572
600
|
| Valkey | `valkey-cli` | `iredis` (compatible) | - |
|
|
573
601
|
| ClickHouse | `clickhouse-client` | - | `usql` |
|
|
@@ -779,7 +807,6 @@ See [TODO.md](TODO.md) for the complete roadmap.
|
|
|
779
807
|
|
|
780
808
|
### v1.2 - Additional Engines
|
|
781
809
|
- **CockroachDB** - Distributed PostgreSQL-compatible database
|
|
782
|
-
- **FerretDB** - MongoDB-compatible database built on PostgreSQL
|
|
783
810
|
|
|
784
811
|
### v1.3 - Advanced Features
|
|
785
812
|
- Container templates for common configurations
|
|
@@ -803,6 +830,7 @@ The following engines may be added based on community interest:
|
|
|
803
830
|
|
|
804
831
|
- **Local only** - Databases bind to `127.0.0.1`. Remote connection support planned for v1.1.
|
|
805
832
|
- **ClickHouse Windows** - Not supported (hostdb doesn't build for Windows).
|
|
833
|
+
- **FerretDB Windows** - Not supported (postgresql-documentdb has startup issues on Windows).
|
|
806
834
|
- **Qdrant & Meilisearch** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
|
|
807
835
|
|
|
808
836
|
---
|
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 {
|