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
|
@@ -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:
|
|
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
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
542
|
+
`${containers.length} container(s): ${parts.join('; ')} ${chalk.gray('— type to filter')}`,
|
|
540
543
|
),
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
549
|
+
const selectedContainer = await filterableListPrompt(
|
|
550
|
+
allChoices,
|
|
551
|
+
'Select a container:',
|
|
547
552
|
{
|
|
548
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
: chalk.
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
: chalk.
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
677
|
-
? `${chalk.
|
|
678
|
-
:
|
|
679
|
-
|
|
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
|
-
|
|
687
|
-
? `${chalk.cyan('
|
|
688
|
-
:
|
|
689
|
-
|
|
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('
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
: chalk.
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
: chalk.
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
746
|
-
? `${chalk.red('✕')} Delete container
|
|
747
|
-
:
|
|
748
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
127
|
+
`Total: ${engines.length} engine(s), ${formatBytes(totalSize)} ${chalk.gray('— type to filter')}`,
|
|
109
128
|
),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
133
|
+
const action = await filterableListPrompt(
|
|
134
|
+
allChoices,
|
|
135
|
+
'Select an engine:',
|
|
116
136
|
{
|
|
117
|
-
|
|
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)}
|
|
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
|
|
59
|
+
{ name: `${chalk.green('+')} Create container`, value: 'create' },
|
|
60
60
|
]
|
|
61
61
|
: [
|
|
62
|
-
{ name: `${chalk.green('+')} 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
|
|
68
|
-
: chalk.gray('▶ Start
|
|
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
|
|
75
|
-
: chalk.gray('■ Stop
|
|
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('
|
|
96
|
-
: chalk.gray('
|
|
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
|
|
104
|
-
: chalk.gray('⚙ Manage
|
|
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.
|
|
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
|
|
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',
|
|
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
|
}
|
package/cli/constants.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
// Engine icons -
|
|
2
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
}
|