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.
@@ -31,6 +31,8 @@ import {
31
31
  promptDatabaseName,
32
32
  promptFileDatabasePath,
33
33
  escapeablePrompt,
34
+ filterableListPrompt,
35
+ type FilterableChoice,
34
36
  BACK_VALUE,
35
37
  MAIN_MENU_VALUE,
36
38
  } from '../../ui/prompts'
@@ -468,7 +470,7 @@ export async function handleList(
468
470
  const COL_SIZE = 9
469
471
 
470
472
  // Build selectable choices with formatted display (like engines menu)
471
- const containerChoices: MenuChoice[] = containers.map((c, i) => {
473
+ const containerChoices: FilterableChoice[] = containers.map((c, i) => {
472
474
  const size = sizes[i]
473
475
  const isFileBased = isFileBasedEngine(c.engine)
474
476
 
@@ -532,26 +534,27 @@ export async function handleList(
532
534
  )
533
535
  }
534
536
 
535
- // Add separator with summary and actions
536
- containerChoices.push(new inquirer.Separator(chalk.gray('─'.repeat(60))))
537
- containerChoices.push(
537
+ // Build the full choice list with footer items
538
+ const allChoices: (FilterableChoice | inquirer.Separator)[] = [
539
+ ...containerChoices,
540
+ new inquirer.Separator(chalk.gray('─'.repeat(60))),
538
541
  new inquirer.Separator(
539
- chalk.gray(`${containers.length} container(s): ${parts.join('; ')}`),
542
+ `${containers.length} container(s): ${parts.join('; ')} ${chalk.gray('— type to filter')}`,
540
543
  ),
541
- )
542
- containerChoices.push(new inquirer.Separator())
543
- containerChoices.push({ name: `${chalk.green('+')} Create new`, value: 'create' })
544
- containerChoices.push({ name: `${chalk.blue('←')} Back to main menu ${chalk.gray('(esc)')}`, value: 'back' })
544
+ new inquirer.Separator(),
545
+ { name: `${chalk.green('+')} Create new`, value: 'create' },
546
+ { name: `${chalk.blue('')} Back to main menu ${chalk.gray('(esc)')}`, value: 'back' },
547
+ ]
545
548
 
546
- const { selectedContainer } = await escapeablePrompt<{ selectedContainer: string }>([
549
+ const selectedContainer = await filterableListPrompt(
550
+ allChoices,
551
+ 'Select a container:',
547
552
  {
548
- type: 'list',
549
- name: 'selectedContainer',
550
- message: 'Select a container:',
551
- choices: containerChoices,
553
+ filterableCount: containerChoices.length,
552
554
  pageSize: 15,
555
+ emptyText: 'No containers match filter',
553
556
  },
554
- ])
557
+ )
555
558
 
556
559
  // Back returns to main menu (escape is handled globally)
557
560
  if (selectedContainer === 'back') {
@@ -630,19 +633,21 @@ export async function showContainerSubmenu(
630
633
  }
631
634
  }
632
635
 
636
+ // Helper for disabled menu items - includes grayed hint in the name
637
+ const disabledItem = (icon: string, label: string, hint: string) => ({
638
+ name: chalk.gray(`${icon} ${label}`) + chalk.gray(` (${hint})`),
639
+ value: '_disabled_',
640
+ disabled: true, // true hides inquirer's default reason text
641
+ })
642
+
633
643
  // Open shell - always enabled for file-based DBs (if file exists), server databases need to be running
634
644
  const canOpenShell = isFileBasedDB ? existsSync(config.database) : isRunning
635
- actionChoices.push({
636
- name: canOpenShell
637
- ? `${chalk.blue('⌘')} Open shell`
638
- : chalk.gray(' Open shell'),
639
- value: 'shell',
640
- disabled: canOpenShell
641
- ? false
642
- : isFileBasedDB
643
- ? 'Database file missing'
644
- : 'Start container first',
645
- })
645
+ const shellHint = isFileBasedDB ? 'Database file missing' : 'Start container first'
646
+ actionChoices.push(
647
+ canOpenShell
648
+ ? { name: `${chalk.blue('>')} Open shell`, value: 'shell' }
649
+ : disabledItem('>', 'Open shell', shellHint),
650
+ )
646
651
 
647
652
  // Run SQL/script - always enabled for file-based DBs (if file exists), server databases need to be running
648
653
  // REST API engines (Qdrant, Meilisearch, CouchDB) don't support script files - hide the option entirely
@@ -657,71 +662,52 @@ export async function showContainerSubmenu(
657
662
  : config.engine === Engine.SurrealDB
658
663
  ? 'Run SurrealQL file'
659
664
  : 'Run SQL file'
660
- actionChoices.push({
661
- name: canRunSql
662
- ? `${chalk.yellow('▷')} ${runScriptLabel}`
663
- : chalk.gray(`▷ ${runScriptLabel}`),
664
- value: 'run-sql',
665
- disabled: canRunSql
666
- ? false
667
- : isFileBasedDB
668
- ? 'Database file missing'
669
- : 'Start container first',
670
- })
665
+ const runSqlHint = isFileBasedDB ? 'Database file missing' : 'Start container first'
666
+ actionChoices.push(
667
+ canRunSql
668
+ ? { name: `${chalk.yellow('▷')} ${runScriptLabel}`, value: 'run-sql' }
669
+ : disabledItem('', runScriptLabel, runSqlHint),
670
+ )
671
671
  }
672
672
 
673
673
  // Edit container - file-based DBs can always edit (no running state), server databases must be stopped
674
674
  const canEdit = isFileBasedDB ? true : !isRunning
675
- actionChoices.push({
676
- name: canEdit
677
- ? `${chalk.white('⚙')} Edit container`
678
- : chalk.gray('⚙ Edit container'),
679
- value: 'edit',
680
- disabled: canEdit ? false : 'Stop container first',
681
- })
675
+ actionChoices.push(
676
+ canEdit
677
+ ? { name: `${chalk.yellow('⚙')} Edit container`, value: 'edit' }
678
+ : disabledItem('⚙', 'Edit container', 'Stop container first'),
679
+ )
682
680
 
683
681
  // Clone container - file-based DBs can always clone, server databases must be stopped
684
682
  const canClone = isFileBasedDB ? true : !isRunning
685
- actionChoices.push({
686
- name: canClone
687
- ? `${chalk.cyan('')} Clone container`
688
- : chalk.gray(' Clone container'),
689
- value: 'clone',
690
- disabled: canClone ? false : 'Stop container first',
691
- })
683
+ actionChoices.push(
684
+ canClone
685
+ ? { name: `${chalk.cyan('')} Clone container`, value: 'clone' }
686
+ : disabledItem('◇', 'Clone container', 'Stop container first'),
687
+ )
692
688
 
693
689
  actionChoices.push({
694
- name: `${chalk.magenta('')} Copy connection string`,
690
+ name: `${chalk.magenta('')} Copy connection string`,
695
691
  value: 'copy',
696
692
  })
697
693
 
698
694
  // Backup - requires running for server databases, file exists for file-based DBs
699
695
  const canBackup = isFileBasedDB ? existsSync(config.database) : isRunning
700
- actionChoices.push({
701
- name: canBackup
702
- ? `${chalk.magenta('↓')} Backup database`
703
- : chalk.gray('↓ Backup database'),
704
- value: 'backup',
705
- disabled: canBackup
706
- ? false
707
- : isFileBasedDB
708
- ? 'Database file missing'
709
- : 'Start container first',
710
- })
696
+ const backupHint = isFileBasedDB ? 'Database file missing' : 'Start container first'
697
+ actionChoices.push(
698
+ canBackup
699
+ ? { name: `${chalk.magenta('↓')} Backup database`, value: 'backup' }
700
+ : disabledItem('', 'Backup database', backupHint),
701
+ )
711
702
 
712
703
  // Restore - requires running for server databases, file exists for file-based DBs
713
704
  const canRestore = isFileBasedDB ? existsSync(config.database) : isRunning
714
- actionChoices.push({
715
- name: canRestore
716
- ? `${chalk.magenta('↑')} Restore from backup`
717
- : chalk.gray('↑ Restore from backup'),
718
- value: 'restore',
719
- disabled: canRestore
720
- ? false
721
- : isFileBasedDB
722
- ? 'Database file missing'
723
- : 'Start container first',
724
- })
705
+ const restoreHint = isFileBasedDB ? 'Database file missing' : 'Start container first'
706
+ actionChoices.push(
707
+ canRestore
708
+ ? { name: `${chalk.magenta('↑')} Restore from backup`, value: 'restore' }
709
+ : disabledItem('', 'Restore from backup', restoreHint),
710
+ )
725
711
 
726
712
  // View logs - not available for file-based DBs (no log file)
727
713
  if (!isFileBasedDB) {
@@ -741,13 +727,11 @@ export async function showContainerSubmenu(
741
727
 
742
728
  // Delete container - file-based DBs can always delete, server databases must be stopped
743
729
  const canDelete = isFileBasedDB ? true : !isRunning
744
- actionChoices.push({
745
- name: canDelete
746
- ? `${chalk.red('✕')} Delete container`
747
- : chalk.gray('✕ Delete container'),
748
- value: 'delete',
749
- disabled: canDelete ? false : 'Stop container first',
750
- })
730
+ actionChoices.push(
731
+ canDelete
732
+ ? { name: `${chalk.red('✕')} Delete container`, value: 'delete' }
733
+ : disabledItem('✕', 'Delete container', 'Stop container first'),
734
+ )
751
735
 
752
736
  actionChoices.push(
753
737
  new inquirer.Separator(),
@@ -5,7 +5,12 @@ import { join, dirname, basename } from 'path'
5
5
  import { containerManager } from '../../../core/container-manager'
6
6
  import { createSpinner } from '../../ui/spinner'
7
7
  import { header, uiError, uiWarning, uiInfo, formatBytes } from '../../ui/theme'
8
- import { promptConfirm, escapeablePrompt } from '../../ui/prompts'
8
+ import {
9
+ promptConfirm,
10
+ escapeablePrompt,
11
+ filterableListPrompt,
12
+ type FilterableChoice,
13
+ } from '../../ui/prompts'
9
14
  import { getEngineIcon, getEngineIconPadded } from '../../constants'
10
15
  import {
11
16
  getInstalledEngines,
@@ -21,6 +26,10 @@ import {
21
26
  type InstalledClickHouseEngine,
22
27
  type InstalledQdrantEngine,
23
28
  type InstalledMeilisearchEngine,
29
+ type InstalledCouchDBEngine,
30
+ type InstalledCockroachDBEngine,
31
+ type InstalledSurrealDBEngine,
32
+ type InstalledQuestDBEngine,
24
33
  } from '../../helpers'
25
34
 
26
35
  import { type MenuChoice } from './shared'
@@ -68,6 +77,14 @@ export async function handleEngines(): Promise<void> {
68
77
  ...engines.filter(
69
78
  (e): e is InstalledMeilisearchEngine => e.engine === 'meilisearch',
70
79
  ),
80
+ ...engines.filter((e): e is InstalledCouchDBEngine => e.engine === 'couchdb'),
81
+ ...engines.filter(
82
+ (e): e is InstalledCockroachDBEngine => e.engine === 'cockroachdb',
83
+ ),
84
+ ...engines.filter(
85
+ (e): e is InstalledSurrealDBEngine => e.engine === 'surrealdb',
86
+ ),
87
+ ...engines.filter((e): e is InstalledQuestDBEngine => e.engine === 'questdb'),
71
88
  ]
72
89
 
73
90
  // Calculate total size
@@ -81,7 +98,7 @@ export async function handleEngines(): Promise<void> {
81
98
  const COL_SIZE = 10
82
99
 
83
100
  // Build selectable choices with formatted display
84
- const choices: MenuChoice[] = allEnginesSorted.map((e) => {
101
+ const engineChoices: FilterableChoice[] = allEnginesSorted.map((e) => {
85
102
  // Use getEngineIconPadded to handle emoji width inconsistencies
86
103
  // Icons like 🦭 and 🪶 render at width 1, others at width 2
87
104
  const icon = getEngineIconPadded(e.engine)
@@ -102,25 +119,26 @@ export async function handleEngines(): Promise<void> {
102
119
  }
103
120
  })
104
121
 
105
- choices.push(new inquirer.Separator(chalk.gray('─'.repeat(52))))
106
- choices.push(
122
+ // Build full choice list with footer
123
+ const allChoices: (FilterableChoice | inquirer.Separator)[] = [
124
+ ...engineChoices,
125
+ new inquirer.Separator(chalk.gray('─'.repeat(52))),
107
126
  new inquirer.Separator(
108
- chalk.gray(`Total: ${engines.length} engine(s), ${formatBytes(totalSize)}`),
127
+ `Total: ${engines.length} engine(s), ${formatBytes(totalSize)} ${chalk.gray('— type to filter')}`,
109
128
  ),
110
- )
111
- choices.push(new inquirer.Separator())
112
- choices.push({ name: `${chalk.blue('←')} Back to main menu ${chalk.gray('(esc)')}`, value: 'back' })
113
- choices.push(new inquirer.Separator()) // Separator for when list wraps around
129
+ new inquirer.Separator(),
130
+ { name: `${chalk.blue('←')} Back to main menu ${chalk.gray('(esc)')}`, value: 'back' },
131
+ ]
114
132
 
115
- const { action } = await escapeablePrompt<{ action: string }>([
133
+ const action = await filterableListPrompt(
134
+ allChoices,
135
+ 'Select an engine:',
116
136
  {
117
- type: 'list',
118
- name: 'action',
119
- message: 'Select an engine:',
120
- choices,
137
+ filterableCount: engineChoices.length,
121
138
  pageSize: 18,
139
+ emptyText: 'No engines match filter',
122
140
  },
123
- ])
141
+ )
124
142
 
125
143
  // Back returns to main menu (escape is handled globally)
126
144
  if (action === 'back') {
@@ -164,7 +182,7 @@ async function showEngineSubmenu(
164
182
  console.log()
165
183
  console.log(
166
184
  chalk.cyan(
167
- ` ${getEngineIcon(engineName)} ${engineName} ${engineVersion} ${chalk.gray(`(${formatBytes(sizeBytes)})`)}`,
185
+ ` ${getEngineIcon(engineName)}${engineName} ${engineVersion} ${chalk.gray(`(${formatBytes(sizeBytes)})`)}`,
168
186
  ),
169
187
  )
170
188
  console.log()
@@ -56,23 +56,23 @@ async function showMainMenu(): Promise<void> {
56
56
  ...(hasContainers
57
57
  ? [
58
58
  { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
59
- { name: `${chalk.green('+')} Create new container`, value: 'create' },
59
+ { name: `${chalk.green('+')} Create container`, value: 'create' },
60
60
  ]
61
61
  : [
62
- { name: `${chalk.green('+')} Create new container`, value: 'create' },
62
+ { name: `${chalk.green('+')} Create container`, value: 'create' },
63
63
  { name: `${chalk.cyan('◉')} Containers`, value: 'list' },
64
64
  ]),
65
65
  {
66
66
  name: canStart
67
- ? `${chalk.green('▶')} Start a container`
68
- : chalk.gray('▶ Start a container'),
67
+ ? `${chalk.green('▶')} Start container`
68
+ : chalk.gray('▶ Start container'),
69
69
  value: 'start',
70
70
  disabled: canStart ? false : 'No stopped containers',
71
71
  },
72
72
  {
73
73
  name: canStop
74
- ? `${chalk.red('■')} Stop a container`
75
- : chalk.gray('■ Stop a container'),
74
+ ? `${chalk.red('■')} Stop container`
75
+ : chalk.gray('■ Stop container'),
76
76
  value: 'stop',
77
77
  disabled: canStop ? false : 'No running containers',
78
78
  },
@@ -92,22 +92,22 @@ async function showMainMenu(): Promise<void> {
92
92
  },
93
93
  {
94
94
  name: canClone
95
- ? `${chalk.cyan('')} Clone a container`
96
- : chalk.gray(' Clone a container'),
95
+ ? `${chalk.cyan('')} Clone container`
96
+ : chalk.gray(' Clone container'),
97
97
  value: 'clone',
98
98
  disabled: canClone ? false : 'No containers',
99
99
  },
100
100
  new inquirer.Separator(),
101
101
  {
102
102
  name: hasEngines
103
- ? `${chalk.yellow('⚙')} Manage installed engines`
104
- : chalk.gray('⚙ Manage installed engines'),
103
+ ? `${chalk.yellow('⚙')} Manage engines`
104
+ : chalk.gray('⚙ Manage engines'),
105
105
  value: 'engines',
106
106
  disabled: hasEngines ? false : 'No engines installed',
107
107
  },
108
- { name: `${chalk.bgRed.white('+')} System health check`, value: 'doctor' },
108
+ { name: `${chalk.red.bold('+')} Health check`, value: 'doctor' },
109
109
  { name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
110
- { name: `${chalk.gray('⏻')} Exit ${chalk.gray('(ctrl+c)')}`, value: 'exit' },
110
+ { name: `${chalk.gray('⏻')} Exit`, value: 'exit' },
111
111
  ]
112
112
 
113
113
  const { action } = await escapeablePrompt<{ action: string }>([
@@ -222,6 +222,15 @@ export async function handleOpenShell(containerName: string): Promise<void> {
222
222
  engineSpecificInstalled = false
223
223
  engineSpecificValue = null
224
224
  engineSpecificInstallValue = null
225
+ } else if (config.engine === 'questdb') {
226
+ // QuestDB uses PostgreSQL wire protocol, can use psql or Web Console
227
+ // Note: Don't recommend pgcli for QuestDB - pgcli uses PostgreSQL functions
228
+ // like unnest() that QuestDB doesn't support, causing autocompletion errors
229
+ defaultShellName = 'psql'
230
+ engineSpecificCli = null
231
+ engineSpecificInstalled = false
232
+ engineSpecificValue = null
233
+ engineSpecificInstallValue = null
225
234
  } else if (config.engine === 'cockroachdb') {
226
235
  // CockroachDB uses cockroach sql command
227
236
  defaultShellName = 'cockroach sql'
@@ -919,6 +928,15 @@ async function launchShell(
919
928
  config.database,
920
929
  ]
921
930
  installHint = 'spindb engines download cockroachdb'
931
+ } else if (config.engine === 'questdb') {
932
+ // QuestDB uses PostgreSQL wire protocol on port 8812
933
+ // Default credentials: admin/quest
934
+ shellCmd = 'psql'
935
+ const db = config.database || 'qdb'
936
+ // QuestDB connection string with explicit password
937
+ const questDbConnStr = `postgresql://admin:quest@127.0.0.1:${config.port}/${db}`
938
+ shellArgs = [questDbConnStr]
939
+ installHint = 'brew install libpq && brew link --force libpq'
922
940
  } else {
923
941
  shellCmd = 'psql'
924
942
  shellArgs = [connectionString]
@@ -954,6 +972,14 @@ async function launchShell(
954
972
  settle()
955
973
  })
956
974
 
957
- shellProcess.on('close', settle)
975
+ shellProcess.on('close', () => {
976
+ // Clear terminal to remove any residual graphics from shells (e.g., usql logo)
977
+ // Use aggressive ANSI sequences: clear screen + scrollback + reset cursor
978
+ // Only emit ANSI escape codes when output is a TTY
979
+ if (process.stdout.isTTY) {
980
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H')
981
+ }
982
+ settle()
983
+ })
958
984
  })
959
985
  }
@@ -92,6 +92,7 @@ export async function handleRunSql(containerName: string): Promise<void> {
92
92
  case Engine.DuckDB:
93
93
  case Engine.ClickHouse:
94
94
  case Engine.CockroachDB:
95
+ case Engine.QuestDB:
95
96
  return { type: 'SQL', lower: 'sql' }
96
97
 
97
98
  default:
package/cli/constants.ts CHANGED
@@ -1,5 +1,7 @@
1
- // Engine icons - do NOT add trailing spaces here
2
- export const ENGINE_ICONS: Record<string, string> = {
1
+ // Engine icons - raw emojis without trailing spaces
2
+ // Use getEngineIcon() to get the icon with consistent spacing
3
+ // NOTE: Avoid variation selectors (U+FE0F) - they cause inconsistent width rendering
4
+ const ENGINE_ICONS: Record<string, string> = {
3
5
  postgresql: '🐘',
4
6
  mysql: '🐬',
5
7
  mariadb: '🦭',
@@ -15,46 +17,46 @@ export const ENGINE_ICONS: Record<string, string> = {
15
17
  couchdb: '🛋',
16
18
  cockroachdb: '🪳',
17
19
  surrealdb: '🌀',
20
+ questdb: '⏱',
18
21
  }
19
22
 
20
- // Visual width of each icon in terminal columns
21
- // Most emojis render at width 2, but some render narrower (width 1)
22
- // This map allows us to pad icons correctly for column alignment
23
- export const ENGINE_ICON_WIDTHS: Record<string, number> = {
24
- postgresql: 2,
25
- mysql: 2,
26
- mariadb: 1, // 🦭 seal renders narrow
27
- sqlite: 1, // 🪶 feather renders narrow
28
- duckdb: 2,
29
- mongodb: 2,
30
- ferretdb: 2,
31
- redis: 2,
32
- valkey: 2,
33
- clickhouse: 2,
34
- qdrant: 2,
35
- meilisearch: 2,
36
- couchdb: 1, // 🛋 couch renders narrow
37
- cockroachdb: 1, // 🪳 cockroach renders narrow
38
- surrealdb: 2, // 🌀 cyclone renders at standard width
39
- }
23
+ const DEFAULT_ENGINE_ICON = '▣'
24
+
25
+ // Emojis that render as 1 cell (narrow) in specific terminals
26
+ // These need extra padding to maintain alignment
27
+ // Based on testing in actual terminals:
28
+ const NARROW_IN_VSCODE = new Set(['🪶', '🦭', '🪳', '🛋', '⏱'])
29
+ const NARROW_IN_GHOSTTY = new Set(['🛋', '⏱'])
40
30
 
41
- export const DEFAULT_ENGINE_ICON = '▣'
42
- export const DEFAULT_ICON_WIDTH = 2
31
+ // Detect terminal
32
+ const isVSCodeTerminal =
33
+ process.env.TERM_PROGRAM === 'vscode' ||
34
+ process.env.TERM_PROGRAM === 'VSCodium'
35
+ const isGhosttyTerminal = process.env.TERM_PROGRAM === 'ghostty'
43
36
 
37
+ /**
38
+ * Returns engine icon with trailing spaces for consistent alignment.
39
+ *
40
+ * Terminal emulators render emoji widths inconsistently.
41
+ * We maintain per-terminal lists of narrow emojis that need extra padding.
42
+ */
44
43
  export function getEngineIcon(engine: string): string {
45
- return ENGINE_ICONS[engine] || DEFAULT_ENGINE_ICON
46
- }
44
+ const icon = ENGINE_ICONS[engine] || DEFAULT_ENGINE_ICON
45
+
46
+ let isNarrow = false
47
+ if (isVSCodeTerminal) {
48
+ isNarrow = NARROW_IN_VSCODE.has(icon)
49
+ } else if (isGhosttyTerminal) {
50
+ isNarrow = NARROW_IN_GHOSTTY.has(icon)
51
+ }
52
+ // Other terminals (iTerm2, Terminal.app) seem to render all emojis as 2 cells
47
53
 
48
- // Returns icon width for alignment calculations
49
- export function getEngineIconWidth(engine: string): number {
50
- return ENGINE_ICON_WIDTHS[engine] ?? DEFAULT_ICON_WIDTH
54
+ return icon + (isNarrow ? ' ' : ' ')
51
55
  }
52
56
 
53
- // Returns icon padded to consistent width (2 columns)
54
- // Use this when displaying icons in aligned columns
55
- export function getEngineIconPadded(engine: string, targetWidth = 2): string {
56
- const icon = ENGINE_ICONS[engine] || DEFAULT_ENGINE_ICON
57
- const width = ENGINE_ICON_WIDTHS[engine] ?? DEFAULT_ICON_WIDTH
58
- const padding = Math.max(0, targetWidth - width)
59
- return icon + ' '.repeat(padding)
57
+ /**
58
+ * @deprecated Use getEngineIcon() instead - it now includes consistent spacing
59
+ */
60
+ export function getEngineIconPadded(engine: string): string {
61
+ return getEngineIcon(engine)
60
62
  }
package/cli/helpers.ts CHANGED
@@ -214,6 +214,16 @@ export type InstalledSurrealDBEngine = {
214
214
  source: 'downloaded'
215
215
  }
216
216
 
217
+ export type InstalledQuestDBEngine = {
218
+ engine: 'questdb'
219
+ version: string
220
+ platform: string
221
+ arch: string
222
+ path: string
223
+ sizeBytes: number
224
+ source: 'downloaded'
225
+ }
226
+
217
227
  export type InstalledEngine =
218
228
  | InstalledPostgresEngine
219
229
  | InstalledMariadbEngine
@@ -230,6 +240,7 @@ export type InstalledEngine =
230
240
  | InstalledCouchDBEngine
231
241
  | InstalledCockroachDBEngine
232
242
  | InstalledSurrealDBEngine
243
+ | InstalledQuestDBEngine
233
244
 
234
245
  async function getPostgresVersion(binPath: string): Promise<string | null> {
235
246
  const ext = platformService.getExecutableExtension()
@@ -1010,6 +1021,62 @@ async function getInstalledSurrealDBEngines(): Promise<InstalledSurrealDBEngine[
1010
1021
  return engines
1011
1022
  }
1012
1023
 
1024
+ // Get QuestDB version from directory path
1025
+ // QuestDB doesn't have a simple --version flag, extract from directory name
1026
+ async function getQuestDBVersion(binPath: string): Promise<string | null> {
1027
+ const platform = platformService.getPlatformInfo().platform
1028
+ // Check for questdb startup script
1029
+ if (platform === 'win32') {
1030
+ if (!existsSync(join(binPath, 'questdb.exe'))) {
1031
+ return null
1032
+ }
1033
+ } else {
1034
+ if (!existsSync(join(binPath, 'questdb.sh'))) {
1035
+ return null
1036
+ }
1037
+ }
1038
+ // Version is embedded in directory name, return null to use directory-parsed version
1039
+ return null
1040
+ }
1041
+
1042
+ // Get installed QuestDB engines from downloaded binaries
1043
+ async function getInstalledQuestDBEngines(): Promise<InstalledQuestDBEngine[]> {
1044
+ const binDir = paths.bin
1045
+
1046
+ if (!existsSync(binDir)) {
1047
+ return []
1048
+ }
1049
+
1050
+ const entries = await readdir(binDir, { withFileTypes: true })
1051
+ const engines: InstalledQuestDBEngine[] = []
1052
+
1053
+ for (const entry of entries) {
1054
+ if (!entry.isDirectory()) continue
1055
+ if (!entry.name.startsWith('questdb-')) continue
1056
+
1057
+ const parsed = parseEngineDirectory(entry.name, 'questdb-', binDir)
1058
+ if (!parsed) continue
1059
+
1060
+ const actualVersion =
1061
+ (await getQuestDBVersion(parsed.path)) || parsed.version
1062
+ const sizeBytes = await calculateDirectorySize(parsed.path)
1063
+
1064
+ engines.push({
1065
+ engine: 'questdb',
1066
+ version: actualVersion,
1067
+ platform: parsed.platform,
1068
+ arch: parsed.arch,
1069
+ path: parsed.path,
1070
+ sizeBytes,
1071
+ source: 'downloaded',
1072
+ })
1073
+ }
1074
+
1075
+ engines.sort((a, b) => compareVersions(b.version, a.version))
1076
+
1077
+ return engines
1078
+ }
1079
+
1013
1080
  // Get FerretDB version from binary path
1014
1081
  async function getFerretDBVersion(binPath: string): Promise<string | null> {
1015
1082
  const ext = platformService.getExecutableExtension()
@@ -1099,6 +1166,7 @@ const ENGINE_PREFIXES = [
1099
1166
  'couchdb-',
1100
1167
  'cockroachdb-',
1101
1168
  'surrealdb-',
1169
+ 'questdb-',
1102
1170
  ] as const
1103
1171
 
1104
1172
  /**
@@ -1144,6 +1212,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
1144
1212
  couchdbEngines,
1145
1213
  cockroachdbEngines,
1146
1214
  surrealdbEngines,
1215
+ questdbEngines,
1147
1216
  ] = await Promise.all([
1148
1217
  getInstalledPostgresEngines(),
1149
1218
  getInstalledMariadbEngines(),
@@ -1160,6 +1229,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
1160
1229
  getInstalledCouchDBEngines(),
1161
1230
  getInstalledCockroachDBEngines(),
1162
1231
  getInstalledSurrealDBEngines(),
1232
+ getInstalledQuestDBEngines(),
1163
1233
  ])
1164
1234
 
1165
1235
  return [
@@ -1178,6 +1248,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
1178
1248
  ...couchdbEngines,
1179
1249
  ...cockroachdbEngines,
1180
1250
  ...surrealdbEngines,
1251
+ ...questdbEngines,
1181
1252
  ]
1182
1253
  }
1183
1254
 
@@ -1196,4 +1267,5 @@ export {
1196
1267
  getInstalledCouchDBEngines,
1197
1268
  getInstalledCockroachDBEngines,
1198
1269
  getInstalledSurrealDBEngines,
1270
+ getInstalledQuestDBEngines,
1199
1271
  }