spindb 0.26.2 → 0.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -10
- package/cli/commands/backups.ts +6 -17
- package/cli/commands/config.ts +5 -4
- package/cli/commands/engines.ts +95 -43
- package/cli/commands/info.ts +4 -4
- package/cli/commands/list.ts +3 -9
- package/cli/commands/menu/backup-handlers.ts +12 -3
- package/cli/commands/menu/container-handlers.ts +65 -81
- package/cli/commands/menu/engine-handlers.ts +34 -16
- package/cli/commands/menu/index.ts +12 -12
- package/cli/commands/menu/shell-handlers.ts +27 -1
- package/cli/commands/menu/sql-handlers.ts +1 -0
- package/cli/constants.ts +38 -36
- package/cli/helpers.ts +72 -0
- package/cli/ui/prompts.ts +112 -1
- package/cli/ui/theme.ts +0 -2
- package/config/backup-formats.ts +14 -0
- package/config/engine-defaults.ts +13 -0
- package/config/engines.json +16 -0
- package/core/config-manager.ts +10 -0
- package/core/container-manager.ts +8 -6
- package/core/dependency-manager.ts +2 -0
- package/engines/index.ts +4 -0
- package/engines/mariadb/restore.ts +133 -57
- package/engines/mysql/restore.ts +160 -60
- package/engines/questdb/backup.ts +217 -0
- package/engines/questdb/binary-manager.ts +303 -0
- package/engines/questdb/binary-urls.ts +34 -0
- package/engines/questdb/hostdb-releases.ts +101 -0
- package/engines/questdb/index.ts +871 -0
- package/engines/questdb/restore.ts +235 -0
- package/engines/questdb/version-maps.ts +37 -0
- package/engines/questdb/version-validator.ts +121 -0
- package/package.json +3 -1
- package/types/index.ts +9 -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 16 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|ferretdb|redis|valkey|clickhouse|sqlite|duckdb|qdrant|meilisearch|couchdb|cockroachdb|surrealdb]
|
|
51
|
+
spindb create mydb --engine [postgresql|mysql|mariadb|mongodb|ferretdb|redis|valkey|clickhouse|sqlite|duckdb|qdrant|meilisearch|couchdb|cockroachdb|surrealdb|questdb]
|
|
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 **16 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
|
|----------|:-----------:|:-----------:|:---------:|:-----------:|:-----------:|
|
|
@@ -86,11 +86,12 @@ SpinDB works across **15 database engines** and **5 platform architectures** wit
|
|
|
86
86
|
| 🏠 **ClickHouse** | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
87
87
|
| 🧭 **Qdrant** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
88
88
|
| 🔍 **Meilisearch** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
89
|
-
|
|
|
89
|
+
| 🛋️ **CouchDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
90
90
|
| 🪳 **CockroachDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
91
91
|
| 🌀 **SurrealDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
92
|
+
| ⏱️ **QuestDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
92
93
|
|
|
93
|
-
**
|
|
94
|
+
**78 combinations. One CLI. Zero configuration.**
|
|
94
95
|
|
|
95
96
|
---
|
|
96
97
|
|
|
@@ -168,7 +169,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
|
|
|
168
169
|
|
|
169
170
|
| Feature | SpinDB | Docker | DBngin | Postgres.app | XAMPP |
|
|
170
171
|
|---------|--------|--------|--------|--------------|-------|
|
|
171
|
-
| **All database types unified** | ✅
|
|
172
|
+
| **All database types unified** | ✅ 16 engines | ❌ | ❌ | ❌ | ❌ |
|
|
172
173
|
| No Docker required | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
173
174
|
| CLI-first | ✅ | ✅ | ❌ GUI-first | ❌ GUI-first | ❌ GUI-first |
|
|
174
175
|
| Multiple versions side-by-side | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
@@ -183,7 +184,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
|
|
|
183
184
|
|
|
184
185
|
## Supported Databases
|
|
185
186
|
|
|
186
|
-
SpinDB supports **
|
|
187
|
+
SpinDB supports **16 database engines** with **multiple versions** for each:
|
|
187
188
|
|
|
188
189
|
| Engine | Type | Versions | Default Port | Query Language |
|
|
189
190
|
|--------|------|----------|--------------|----------------|
|
|
@@ -199,13 +200,14 @@ SpinDB supports **15 database engines** with **multiple versions** for each:
|
|
|
199
200
|
| 🏠 **ClickHouse** | Columnar OLAP | 25.12 | 9000 (TCP), 8123 (HTTP) | SQL (ClickHouse dialect) |
|
|
200
201
|
| 🧭 **Qdrant** | Vector Search | 1 | 6333 (HTTP), 6334 (gRPC) | REST API |
|
|
201
202
|
| 🔍 **Meilisearch** | Full-Text Search | 1 | 7700 | REST API |
|
|
202
|
-
|
|
|
203
|
+
| 🛋️ **CouchDB** | Document Store | 3 | 5984 | REST API |
|
|
203
204
|
| 🪳 **CockroachDB** | Distributed SQL | 25 | 26257 | SQL (PostgreSQL-compatible) |
|
|
204
205
|
| 🌀 **SurrealDB** | Multi-Model | 2 | 8000 | SurrealQL |
|
|
206
|
+
| ⏱️ **QuestDB** | Time-Series SQL | 9 | 8812 (PG), 9000 (HTTP) | SQL |
|
|
205
207
|
|
|
206
208
|
### Engine Categories
|
|
207
209
|
|
|
208
|
-
**Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, FerretDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch, CouchDB, CockroachDB, SurrealDB):
|
|
210
|
+
**Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, FerretDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch, CouchDB, CockroachDB, SurrealDB, QuestDB):
|
|
209
211
|
- Start/stop server processes
|
|
210
212
|
- Bind to localhost ports
|
|
211
213
|
- Data stored in `~/.spindb/containers/{engine}/{name}/`
|
|
@@ -432,6 +434,7 @@ Databases run as **native processes**, and **data persists across restarts**. Wh
|
|
|
432
434
|
| Valkey | RDB snapshots (periodic) | May lose ~60 seconds on unexpected crash |
|
|
433
435
|
| ClickHouse | MergeTree storage | Committed transactions survive crashes |
|
|
434
436
|
| CockroachDB | Raft consensus | Strongly consistent, distributed replication |
|
|
437
|
+
| QuestDB | Write-ahead logging | Committed transactions survive crashes |
|
|
435
438
|
|
|
436
439
|
---
|
|
437
440
|
|
|
@@ -635,6 +638,7 @@ SpinDB supports enhanced database shells with auto-completion, syntax highlighti
|
|
|
635
638
|
| Meilisearch | REST API | - | - |
|
|
636
639
|
| CouchDB | REST API | - | - |
|
|
637
640
|
| CockroachDB | `cockroach sql` | - | - |
|
|
641
|
+
| QuestDB | `psql` | `pgcli` | `usql` |
|
|
638
642
|
|
|
639
643
|
Install and use in one command:
|
|
640
644
|
|
|
@@ -754,6 +758,18 @@ spindb backup mydb --format snapshot # Snapshot (only format)
|
|
|
754
758
|
spindb backup mydb --format sql # SQL dump (only format)
|
|
755
759
|
```
|
|
756
760
|
|
|
761
|
+
### QuestDB
|
|
762
|
+
|
|
763
|
+
| Format | Extension | Tool | Use Case |
|
|
764
|
+
|--------|-----------|------|----------|
|
|
765
|
+
| sql | `.sql` | psql (PostgreSQL wire protocol) | Plain SQL dump |
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
spindb backup mydb --format sql # SQL dump (only format)
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
> **Note:** QuestDB backup/restore requires the PostgreSQL engine to be installed (for `psql`).
|
|
772
|
+
|
|
757
773
|
---
|
|
758
774
|
|
|
759
775
|
## Advanced Features
|
|
@@ -797,6 +813,7 @@ spindb restore mydb --from-url "postgresql://user:pass@prod-host:5432/production
|
|
|
797
813
|
| Meilisearch | `meilisearch://` or `http://` | `http://host:7700?api_key=KEY` |
|
|
798
814
|
| CouchDB | `couchdb://` or `http://` | `http://user:pass@host:5984/db` |
|
|
799
815
|
| CockroachDB | `postgresql://` or `postgres://` | `postgresql://root@host:26257/db?sslmode=disable` |
|
|
816
|
+
| QuestDB | `postgresql://` or `postgres://` | `postgresql://admin:quest@host:8812/qdb` |
|
|
800
817
|
|
|
801
818
|
### Multi-Version Support
|
|
802
819
|
|
|
@@ -949,7 +966,7 @@ See [FEATURE.md](FEATURE.md) for adding new database engines.
|
|
|
949
966
|
|
|
950
967
|
SpinDB is powered by:
|
|
951
968
|
|
|
952
|
-
- **[hostdb](https://github.com/robertjbass/hostdb)** - Pre-compiled database binaries for
|
|
969
|
+
- **[hostdb](https://github.com/robertjbass/hostdb)** - Pre-compiled database binaries for 16 engines across all major platforms. Makes Docker-free multi-version database support possible.
|
|
953
970
|
|
|
954
971
|
---
|
|
955
972
|
|
package/cli/commands/backups.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { join, extname } from 'path'
|
|
|
11
11
|
import { homedir } from 'os'
|
|
12
12
|
import chalk from 'chalk'
|
|
13
13
|
import { formatBytes } from '../ui/theme'
|
|
14
|
+
import { getEngineIcon } from '../constants'
|
|
14
15
|
|
|
15
16
|
type BackupInfo = {
|
|
16
17
|
filename: string
|
|
@@ -132,22 +133,10 @@ function formatRelativeTime(date: Date): string {
|
|
|
132
133
|
return date.toLocaleDateString()
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
// Get engine icon
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return '🐘'
|
|
140
|
-
case 'mysql':
|
|
141
|
-
return '🐬'
|
|
142
|
-
case 'sqlite':
|
|
143
|
-
return '🗄️'
|
|
144
|
-
case 'mongodb':
|
|
145
|
-
return '🍃'
|
|
146
|
-
case 'redis':
|
|
147
|
-
return '🔴'
|
|
148
|
-
default:
|
|
149
|
-
return '📦'
|
|
150
|
-
}
|
|
136
|
+
// Get engine icon - wraps the shared function with fallback for null/unknown engines
|
|
137
|
+
function getBackupEngineIcon(engine: string | null): string {
|
|
138
|
+
if (!engine) return '📦 '
|
|
139
|
+
return getEngineIcon(engine)
|
|
151
140
|
}
|
|
152
141
|
|
|
153
142
|
export const backupsCommand = new Command('backups')
|
|
@@ -230,7 +219,7 @@ export const backupsCommand = new Command('backups')
|
|
|
230
219
|
)
|
|
231
220
|
|
|
232
221
|
for (const backup of limitedBackups) {
|
|
233
|
-
const icon =
|
|
222
|
+
const icon = getBackupEngineIcon(backup.engine)
|
|
234
223
|
const filename =
|
|
235
224
|
backup.filename.length > maxFilename
|
|
236
225
|
? backup.filename.slice(0, maxFilename - 3) + '...'
|
package/cli/commands/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../../core/config-manager'
|
|
11
11
|
import { updateManager } from '../../core/update-manager'
|
|
12
12
|
import { uiError, uiSuccess, header, uiInfo } from '../ui/theme'
|
|
13
|
+
import { getEngineIcon } from '../constants'
|
|
13
14
|
import { createSpinner } from '../ui/spinner'
|
|
14
15
|
import type { BinaryTool } from '../../types'
|
|
15
16
|
|
|
@@ -58,7 +59,7 @@ export const configCommand = new Command('config')
|
|
|
58
59
|
console.log()
|
|
59
60
|
|
|
60
61
|
// PostgreSQL tools
|
|
61
|
-
console.log(chalk.bold('
|
|
62
|
+
console.log(chalk.bold(` ${getEngineIcon('postgresql')}PostgreSQL Tools:`))
|
|
62
63
|
console.log(chalk.gray(' ' + '─'.repeat(60)))
|
|
63
64
|
for (const tool of POSTGRESQL_TOOLS) {
|
|
64
65
|
displayToolConfig(tool, config.binaries[tool])
|
|
@@ -66,7 +67,7 @@ export const configCommand = new Command('config')
|
|
|
66
67
|
console.log()
|
|
67
68
|
|
|
68
69
|
// MySQL tools
|
|
69
|
-
console.log(chalk.bold('
|
|
70
|
+
console.log(chalk.bold(` ${getEngineIcon('mysql')}MySQL Tools:`))
|
|
70
71
|
console.log(chalk.gray(' ' + '─'.repeat(60)))
|
|
71
72
|
for (const tool of MYSQL_TOOLS) {
|
|
72
73
|
displayToolConfig(tool, config.binaries[tool])
|
|
@@ -156,13 +157,13 @@ export const configCommand = new Command('config')
|
|
|
156
157
|
|
|
157
158
|
await displayCategory(
|
|
158
159
|
'PostgreSQL Tools',
|
|
159
|
-
'
|
|
160
|
+
getEngineIcon('postgresql'),
|
|
160
161
|
result.postgresql.found,
|
|
161
162
|
result.postgresql.missing,
|
|
162
163
|
)
|
|
163
164
|
await displayCategory(
|
|
164
165
|
'MySQL Tools',
|
|
165
|
-
'
|
|
166
|
+
getEngineIcon('mysql'),
|
|
166
167
|
result.mysql.found,
|
|
167
168
|
result.mysql.missing,
|
|
168
169
|
)
|
package/cli/commands/engines.ts
CHANGED
|
@@ -29,7 +29,7 @@ import type { BinaryTool } from '../../types'
|
|
|
29
29
|
import { promptConfirm } from '../ui/prompts'
|
|
30
30
|
import { createSpinner } from '../ui/spinner'
|
|
31
31
|
import { uiError, uiWarning, uiInfo, uiSuccess, formatBytes } from '../ui/theme'
|
|
32
|
-
import { getEngineIcon
|
|
32
|
+
import { getEngineIcon } from '../constants'
|
|
33
33
|
import {
|
|
34
34
|
getInstalledEngines,
|
|
35
35
|
getInstalledPostgresEngines,
|
|
@@ -64,18 +64,12 @@ import { ferretdbBinaryManager } from '../../engines/ferretdb/binary-manager'
|
|
|
64
64
|
import { couchdbBinaryManager } from '../../engines/couchdb/binary-manager'
|
|
65
65
|
import { cockroachdbBinaryManager } from '../../engines/cockroachdb/binary-manager'
|
|
66
66
|
import { surrealdbBinaryManager } from '../../engines/surrealdb/binary-manager'
|
|
67
|
+
import { questdbBinaryManager } from '../../engines/questdb/binary-manager'
|
|
67
68
|
import {
|
|
68
69
|
DEFAULT_DOCUMENTDB_VERSION,
|
|
69
70
|
normalizeDocumentDBVersion,
|
|
70
71
|
} from '../../engines/ferretdb/version-maps'
|
|
71
72
|
|
|
72
|
-
// Pad string to width, accounting for emoji taking 2 display columns
|
|
73
|
-
function padWithEmoji(str: string, width: number): string {
|
|
74
|
-
// Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
|
|
75
|
-
const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
|
|
76
|
-
return str.padEnd(width + emojiCount)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
73
|
// Display manual installation instructions for missing dependencies
|
|
80
74
|
function displayManualInstallInstructions(
|
|
81
75
|
missingDeps: Array<{ dependency: { name: string }; installed: boolean }>,
|
|
@@ -481,13 +475,13 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
481
475
|
|
|
482
476
|
// PostgreSQL rows
|
|
483
477
|
for (const engine of pgEngines) {
|
|
484
|
-
const icon = getEngineIcon(engine.engine)
|
|
485
478
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
486
|
-
|
|
479
|
+
// getEngineIcon() includes trailing space for consistent alignment
|
|
480
|
+
const engineDisplay = `${getEngineIcon(engine.engine)}${engine.engine}`
|
|
487
481
|
|
|
488
482
|
console.log(
|
|
489
483
|
chalk.gray(' ') +
|
|
490
|
-
chalk.cyan(
|
|
484
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
491
485
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
492
486
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
493
487
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -496,13 +490,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
496
490
|
|
|
497
491
|
// MySQL rows
|
|
498
492
|
for (const mysqlEngine of mysqlEngines) {
|
|
499
|
-
const icon = ENGINE_ICONS.mysql
|
|
500
493
|
const platformInfo = `${mysqlEngine.platform}-${mysqlEngine.arch}`
|
|
501
|
-
const engineDisplay = `${
|
|
494
|
+
const engineDisplay = `${getEngineIcon('mysql')}mysql`
|
|
502
495
|
|
|
503
496
|
console.log(
|
|
504
497
|
chalk.gray(' ') +
|
|
505
|
-
chalk.cyan(
|
|
498
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
506
499
|
chalk.yellow(mysqlEngine.version.padEnd(12)) +
|
|
507
500
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
508
501
|
chalk.white(formatBytes(mysqlEngine.sizeBytes)),
|
|
@@ -511,12 +504,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
511
504
|
|
|
512
505
|
// SQLite row
|
|
513
506
|
if (sqliteEngine) {
|
|
514
|
-
const
|
|
515
|
-
const engineDisplay = `${icon} sqlite`
|
|
507
|
+
const engineDisplay = `${getEngineIcon('sqlite')}sqlite`
|
|
516
508
|
|
|
517
509
|
console.log(
|
|
518
510
|
chalk.gray(' ') +
|
|
519
|
-
chalk.cyan(
|
|
511
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
520
512
|
chalk.yellow(sqliteEngine.version.padEnd(12)) +
|
|
521
513
|
chalk.gray('system'.padEnd(18)) +
|
|
522
514
|
chalk.gray('(system-installed)'),
|
|
@@ -525,13 +517,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
525
517
|
|
|
526
518
|
// DuckDB rows
|
|
527
519
|
for (const engine of duckdbEngines) {
|
|
528
|
-
const icon = ENGINE_ICONS.duckdb
|
|
529
520
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
530
|
-
const engineDisplay = `${
|
|
521
|
+
const engineDisplay = `${getEngineIcon('duckdb')}duckdb`
|
|
531
522
|
|
|
532
523
|
console.log(
|
|
533
524
|
chalk.gray(' ') +
|
|
534
|
-
chalk.cyan(
|
|
525
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
535
526
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
536
527
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
537
528
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -540,13 +531,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
540
531
|
|
|
541
532
|
// MongoDB rows
|
|
542
533
|
for (const engine of mongodbEngines) {
|
|
543
|
-
const icon = ENGINE_ICONS.mongodb
|
|
544
534
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
545
|
-
const engineDisplay = `${
|
|
535
|
+
const engineDisplay = `${getEngineIcon('mongodb')}mongodb`
|
|
546
536
|
|
|
547
537
|
console.log(
|
|
548
538
|
chalk.gray(' ') +
|
|
549
|
-
chalk.cyan(
|
|
539
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
550
540
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
551
541
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
552
542
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -555,13 +545,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
555
545
|
|
|
556
546
|
// FerretDB rows
|
|
557
547
|
for (const engine of ferretdbEngines) {
|
|
558
|
-
const icon = ENGINE_ICONS.ferretdb
|
|
559
548
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
560
|
-
const engineDisplay = `${
|
|
549
|
+
const engineDisplay = `${getEngineIcon('ferretdb')}ferretdb`
|
|
561
550
|
|
|
562
551
|
console.log(
|
|
563
552
|
chalk.gray(' ') +
|
|
564
|
-
chalk.cyan(
|
|
553
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
565
554
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
566
555
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
567
556
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -570,13 +559,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
570
559
|
|
|
571
560
|
// Redis rows
|
|
572
561
|
for (const engine of redisEngines) {
|
|
573
|
-
const icon = ENGINE_ICONS.redis
|
|
574
562
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
575
|
-
const engineDisplay = `${
|
|
563
|
+
const engineDisplay = `${getEngineIcon('redis')}redis`
|
|
576
564
|
|
|
577
565
|
console.log(
|
|
578
566
|
chalk.gray(' ') +
|
|
579
|
-
chalk.cyan(
|
|
567
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
580
568
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
581
569
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
582
570
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -585,13 +573,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
585
573
|
|
|
586
574
|
// Valkey rows
|
|
587
575
|
for (const engine of valkeyEngines) {
|
|
588
|
-
const icon = ENGINE_ICONS.valkey
|
|
589
576
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
590
|
-
const engineDisplay = `${
|
|
577
|
+
const engineDisplay = `${getEngineIcon('valkey')}valkey`
|
|
591
578
|
|
|
592
579
|
console.log(
|
|
593
580
|
chalk.gray(' ') +
|
|
594
|
-
chalk.cyan(
|
|
581
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
595
582
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
596
583
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
597
584
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -600,13 +587,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
600
587
|
|
|
601
588
|
// Qdrant rows
|
|
602
589
|
for (const engine of qdrantEngines) {
|
|
603
|
-
const icon = ENGINE_ICONS.qdrant
|
|
604
590
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
605
|
-
const engineDisplay = `${
|
|
591
|
+
const engineDisplay = `${getEngineIcon('qdrant')}qdrant`
|
|
606
592
|
|
|
607
593
|
console.log(
|
|
608
594
|
chalk.gray(' ') +
|
|
609
|
-
chalk.cyan(
|
|
595
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
610
596
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
611
597
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
612
598
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -615,13 +601,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
615
601
|
|
|
616
602
|
// Meilisearch rows
|
|
617
603
|
for (const engine of meilisearchEngines) {
|
|
618
|
-
const icon = ENGINE_ICONS.meilisearch
|
|
619
604
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
620
|
-
const engineDisplay = `${
|
|
605
|
+
const engineDisplay = `${getEngineIcon('meilisearch')}meilisearch`
|
|
621
606
|
|
|
622
607
|
console.log(
|
|
623
608
|
chalk.gray(' ') +
|
|
624
|
-
chalk.cyan(
|
|
609
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
625
610
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
626
611
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
627
612
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -630,13 +615,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
|
630
615
|
|
|
631
616
|
// CouchDB rows
|
|
632
617
|
for (const engine of couchdbEngines) {
|
|
633
|
-
const icon = ENGINE_ICONS.couchdb
|
|
634
618
|
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
635
|
-
const engineDisplay = `${
|
|
619
|
+
const engineDisplay = `${getEngineIcon('couchdb')}couchdb`
|
|
636
620
|
|
|
637
621
|
console.log(
|
|
638
622
|
chalk.gray(' ') +
|
|
639
|
-
chalk.cyan(
|
|
623
|
+
chalk.cyan(engineDisplay.padEnd(14)) +
|
|
640
624
|
chalk.yellow(engine.version.padEnd(12)) +
|
|
641
625
|
chalk.gray(platformInfo.padEnd(18)) +
|
|
642
626
|
chalk.white(formatBytes(engine.sizeBytes)),
|
|
@@ -777,7 +761,7 @@ async function deleteEngine(
|
|
|
777
761
|
// Interactive selection if not provided
|
|
778
762
|
if (!engineName || !engineVersion) {
|
|
779
763
|
const choices = pgEngines.map((e) => ({
|
|
780
|
-
name: `${getEngineIcon(e.engine)}
|
|
764
|
+
name: `${getEngineIcon(e.engine)}${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
|
|
781
765
|
value: `${e.engine}:${e.version}:${e.path}`,
|
|
782
766
|
}))
|
|
783
767
|
|
|
@@ -847,6 +831,31 @@ async function deleteEngine(
|
|
|
847
831
|
console.log()
|
|
848
832
|
}
|
|
849
833
|
|
|
834
|
+
// Check for cross-engine dependencies (QuestDB depends on PostgreSQL's psql)
|
|
835
|
+
if (engineName === Engine.PostgreSQL) {
|
|
836
|
+
const questdbContainers = containers.filter(
|
|
837
|
+
(c) => c.engine === Engine.QuestDB,
|
|
838
|
+
)
|
|
839
|
+
if (questdbContainers.length > 0) {
|
|
840
|
+
console.log(
|
|
841
|
+
uiWarning(
|
|
842
|
+
`${questdbContainers.length} QuestDB container(s) depend on PostgreSQL's psql for backup/restore:`,
|
|
843
|
+
),
|
|
844
|
+
)
|
|
845
|
+
console.log(
|
|
846
|
+
chalk.gray(
|
|
847
|
+
` ${questdbContainers.map((c) => c.name).join(', ')}`,
|
|
848
|
+
),
|
|
849
|
+
)
|
|
850
|
+
console.log(
|
|
851
|
+
chalk.gray(
|
|
852
|
+
' Deleting PostgreSQL will break backup/restore for these containers.',
|
|
853
|
+
),
|
|
854
|
+
)
|
|
855
|
+
console.log()
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
850
859
|
const confirmed = await promptConfirm(
|
|
851
860
|
`Delete ${engineName} ${engineVersion}? This cannot be undone.`,
|
|
852
861
|
false,
|
|
@@ -1764,9 +1773,52 @@ enginesCommand
|
|
|
1764
1773
|
return
|
|
1765
1774
|
}
|
|
1766
1775
|
|
|
1776
|
+
if (['questdb', 'quest'].includes(normalizedEngine)) {
|
|
1777
|
+
if (!version) {
|
|
1778
|
+
console.error(uiError('QuestDB requires a version (e.g., 9)'))
|
|
1779
|
+
process.exit(1)
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const engine = getEngine(Engine.QuestDB)
|
|
1783
|
+
|
|
1784
|
+
const spinner = createSpinner(`Checking QuestDB ${version} binaries...`)
|
|
1785
|
+
spinner.start()
|
|
1786
|
+
|
|
1787
|
+
let wasCached = false
|
|
1788
|
+
await engine.ensureBinaries(version, ({ stage, message }) => {
|
|
1789
|
+
if (stage === 'cached') {
|
|
1790
|
+
wasCached = true
|
|
1791
|
+
spinner.text = `QuestDB ${version} binaries ready (cached)`
|
|
1792
|
+
} else {
|
|
1793
|
+
spinner.text = message
|
|
1794
|
+
}
|
|
1795
|
+
})
|
|
1796
|
+
|
|
1797
|
+
if (wasCached) {
|
|
1798
|
+
spinner.succeed(`QuestDB ${version} binaries already installed`)
|
|
1799
|
+
} else {
|
|
1800
|
+
spinner.succeed(`QuestDB ${version} binaries downloaded`)
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Show the path for reference
|
|
1804
|
+
const { platform: questdbPlatform, arch: questdbArch } =
|
|
1805
|
+
platformService.getPlatformInfo()
|
|
1806
|
+
const questdbFullVersion = questdbBinaryManager.getFullVersion(version)
|
|
1807
|
+
const binPath = paths.getBinaryPath({
|
|
1808
|
+
engine: 'questdb',
|
|
1809
|
+
version: questdbFullVersion,
|
|
1810
|
+
platform: questdbPlatform,
|
|
1811
|
+
arch: questdbArch,
|
|
1812
|
+
})
|
|
1813
|
+
console.log(chalk.gray(` Location: ${binPath}`))
|
|
1814
|
+
|
|
1815
|
+
// Skip client tools check for QuestDB - uses psql or Web Console
|
|
1816
|
+
return
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1767
1819
|
console.error(
|
|
1768
1820
|
uiError(
|
|
1769
|
-
`Unknown engine "${engineName}". Supported: postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch, couchdb, cockroachdb, surrealdb`,
|
|
1821
|
+
`Unknown engine "${engineName}". Supported: postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch, couchdb, cockroachdb, surrealdb, questdb`,
|
|
1770
1822
|
),
|
|
1771
1823
|
)
|
|
1772
1824
|
process.exit(1)
|
package/cli/commands/info.ts
CHANGED
|
@@ -81,7 +81,7 @@ async function displayContainerInfo(
|
|
|
81
81
|
console.log(
|
|
82
82
|
chalk.gray(' ') +
|
|
83
83
|
chalk.white('Engine:'.padEnd(14)) +
|
|
84
|
-
chalk.cyan(`${icon}
|
|
84
|
+
chalk.cyan(`${icon}${config.engine} ${config.version}`),
|
|
85
85
|
)
|
|
86
86
|
console.log(
|
|
87
87
|
chalk.gray(' ') + chalk.white('Status:'.padEnd(14)) + statusDisplay,
|
|
@@ -192,8 +192,8 @@ async function displayAllContainersInfo(
|
|
|
192
192
|
: chalk.gray('○ stopped')
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
const engineDisplay = `${
|
|
195
|
+
// getEngineIcon() includes trailing space for consistent alignment
|
|
196
|
+
const engineDisplay = `${getEngineIcon(container.engine)}${container.engine}`
|
|
197
197
|
|
|
198
198
|
// Show truncated file path for SQLite instead of port
|
|
199
199
|
let portOrPath: string
|
|
@@ -290,7 +290,7 @@ export const infoCommand = new Command('info')
|
|
|
290
290
|
choices: [
|
|
291
291
|
{ name: 'All containers', value: 'all' },
|
|
292
292
|
...containers.map((c) => ({
|
|
293
|
-
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)}
|
|
293
|
+
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)}${c.engine})`)}`,
|
|
294
294
|
value: c.name,
|
|
295
295
|
})),
|
|
296
296
|
],
|
package/cli/commands/list.ts
CHANGED
|
@@ -14,12 +14,6 @@ import {
|
|
|
14
14
|
deriveContainerName,
|
|
15
15
|
} from '../../engines/sqlite/scanner'
|
|
16
16
|
|
|
17
|
-
// Pad string to width, accounting for emoji taking 2 display columns
|
|
18
|
-
function padWithEmoji(str: string, width: number): string {
|
|
19
|
-
// Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
|
|
20
|
-
const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
|
|
21
|
-
return str.padEnd(width + emojiCount)
|
|
22
|
-
}
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
19
|
* Prompt user about unregistered SQLite files in CWD
|
|
@@ -194,8 +188,8 @@ export const listCommand = new Command('list')
|
|
|
194
188
|
: chalk.gray('○ stopped')
|
|
195
189
|
}
|
|
196
190
|
|
|
197
|
-
|
|
198
|
-
const engineDisplay = `${
|
|
191
|
+
// getEngineIcon() includes trailing space for consistent alignment
|
|
192
|
+
const engineDisplay = `${getEngineIcon(container.engine)}${container.engine}`
|
|
199
193
|
|
|
200
194
|
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
201
195
|
|
|
@@ -213,7 +207,7 @@ export const listCommand = new Command('list')
|
|
|
213
207
|
console.log(
|
|
214
208
|
chalk.gray(' ') +
|
|
215
209
|
chalk.cyan(container.name.padEnd(20)) +
|
|
216
|
-
chalk.white(
|
|
210
|
+
chalk.white(engineDisplay.padEnd(15)) +
|
|
217
211
|
chalk.yellow(container.version.padEnd(10)) +
|
|
218
212
|
chalk.green(portOrPath.padEnd(8)) +
|
|
219
213
|
chalk.magenta(sizeDisplay.padEnd(10)) +
|
|
@@ -193,6 +193,15 @@ function validateConnectionString(
|
|
|
193
193
|
return 'Connection string must start with surrealdb://, ws://, wss://, http://, or https://'
|
|
194
194
|
}
|
|
195
195
|
break
|
|
196
|
+
case Engine.QuestDB:
|
|
197
|
+
// QuestDB uses PostgreSQL wire protocol
|
|
198
|
+
if (
|
|
199
|
+
!input.startsWith('postgresql://') &&
|
|
200
|
+
!input.startsWith('postgres://')
|
|
201
|
+
) {
|
|
202
|
+
return 'Connection string must start with postgresql:// or postgres://'
|
|
203
|
+
}
|
|
204
|
+
break
|
|
196
205
|
case Engine.SQLite:
|
|
197
206
|
case Engine.DuckDB:
|
|
198
207
|
return 'File-based engines do not support remote connection strings'
|
|
@@ -338,7 +347,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
338
347
|
|
|
339
348
|
const choices = [
|
|
340
349
|
...running.map((c) => ({
|
|
341
|
-
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)}
|
|
350
|
+
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)}${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
|
|
342
351
|
value: c.name,
|
|
343
352
|
short: c.name,
|
|
344
353
|
})),
|
|
@@ -433,7 +442,7 @@ export async function handleRestore(): Promise<void> {
|
|
|
433
442
|
]
|
|
434
443
|
|
|
435
444
|
restoreChoices.push({
|
|
436
|
-
name: `${chalk.cyan('
|
|
445
|
+
name: `${chalk.cyan('↗')} Connection string ${chalk.gray('(pull from remote database)')}`,
|
|
437
446
|
value: 'connection',
|
|
438
447
|
})
|
|
439
448
|
|
|
@@ -1109,7 +1118,7 @@ export async function handleRestoreForContainer(
|
|
|
1109
1118
|
value: 'file',
|
|
1110
1119
|
},
|
|
1111
1120
|
{
|
|
1112
|
-
name: `${chalk.cyan('
|
|
1121
|
+
name: `${chalk.cyan('↗')} Connection string ${chalk.gray('(pull from remote database)')}`,
|
|
1113
1122
|
value: 'connection',
|
|
1114
1123
|
},
|
|
1115
1124
|
]
|