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 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 15 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.
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 **15 database engines** and **5 platform architectures** with a **single, consistent API**.
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
- | 🛋 **CouchDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
89
+ | 🛋️ **CouchDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
90
90
  | 🪳 **CockroachDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
91
91
  | 🌀 **SurrealDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
92
+ | ⏱️ **QuestDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
92
93
 
93
- **73 combinations. One CLI. Zero configuration.**
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** | ✅ 15 engines | ❌ | ❌ | ❌ | ❌ |
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 **15 database engines** with **multiple versions** for each:
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
- | 🛋 **CouchDB** | Document Store | 3 | 5984 | REST API |
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 15 engines across all major platforms. Makes Docker-free multi-version database support possible.
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
 
@@ -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 getEngineIcon(engine: string | null): string {
137
- switch (engine) {
138
- case 'postgresql':
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 = getEngineIcon(backup.engine)
222
+ const icon = getBackupEngineIcon(backup.engine)
234
223
  const filename =
235
224
  backup.filename.length > maxFilename
236
225
  ? backup.filename.slice(0, maxFilename - 3) + '...'
@@ -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(' 🐘 PostgreSQL Tools:'))
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(' 🐬 MySQL Tools:'))
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
  )
@@ -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, ENGINE_ICONS } from '../constants'
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
- const engineDisplay = `${icon} ${engine.engine}`
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(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} mysql`
494
+ const engineDisplay = `${getEngineIcon('mysql')}mysql`
502
495
 
503
496
  console.log(
504
497
  chalk.gray(' ') +
505
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 icon = ENGINE_ICONS.sqlite
515
- const engineDisplay = `${icon} sqlite`
507
+ const engineDisplay = `${getEngineIcon('sqlite')}sqlite`
516
508
 
517
509
  console.log(
518
510
  chalk.gray(' ') +
519
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} duckdb`
521
+ const engineDisplay = `${getEngineIcon('duckdb')}duckdb`
531
522
 
532
523
  console.log(
533
524
  chalk.gray(' ') +
534
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} mongodb`
535
+ const engineDisplay = `${getEngineIcon('mongodb')}mongodb`
546
536
 
547
537
  console.log(
548
538
  chalk.gray(' ') +
549
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} ferretdb`
549
+ const engineDisplay = `${getEngineIcon('ferretdb')}ferretdb`
561
550
 
562
551
  console.log(
563
552
  chalk.gray(' ') +
564
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} redis`
563
+ const engineDisplay = `${getEngineIcon('redis')}redis`
576
564
 
577
565
  console.log(
578
566
  chalk.gray(' ') +
579
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} valkey`
577
+ const engineDisplay = `${getEngineIcon('valkey')}valkey`
591
578
 
592
579
  console.log(
593
580
  chalk.gray(' ') +
594
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} qdrant`
591
+ const engineDisplay = `${getEngineIcon('qdrant')}qdrant`
606
592
 
607
593
  console.log(
608
594
  chalk.gray(' ') +
609
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} meilisearch`
605
+ const engineDisplay = `${getEngineIcon('meilisearch')}meilisearch`
621
606
 
622
607
  console.log(
623
608
  chalk.gray(' ') +
624
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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 = `${icon} couchdb`
619
+ const engineDisplay = `${getEngineIcon('couchdb')}couchdb`
636
620
 
637
621
  console.log(
638
622
  chalk.gray(' ') +
639
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
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)} ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
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)
@@ -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} ${config.engine} ${config.version}`),
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
- const icon = getEngineIcon(container.engine)
196
- const engineDisplay = `${icon} ${container.engine}`
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)} ${c.engine})`)}`,
293
+ name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)}${c.engine})`)}`,
294
294
  value: c.name,
295
295
  })),
296
296
  ],
@@ -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
- const engineIcon = getEngineIcon(container.engine)
198
- const engineDisplay = `${engineIcon} ${container.engine}`
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(padWithEmoji(engineDisplay, 14)) +
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)} ${c.engine} ${c.version}, port ${c.port})`)} ${chalk.green('● running')}`,
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('🔗')} Connection string (pull from remote database)`,
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('🔗')} Connection string (pull from remote database)`,
1121
+ name: `${chalk.cyan('')} Connection string ${chalk.gray('(pull from remote database)')}`,
1113
1122
  value: 'connection',
1114
1123
  },
1115
1124
  ]