spindb 0.27.1 → 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.
@@ -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,
@@ -70,13 +70,6 @@ import {
70
70
  normalizeDocumentDBVersion,
71
71
  } from '../../engines/ferretdb/version-maps'
72
72
 
73
- // Pad string to width, accounting for emoji taking 2 display columns
74
- function padWithEmoji(str: string, width: number): string {
75
- // Count emojis using Extended_Pictographic (excludes digits/symbols that \p{Emoji} matches)
76
- const emojiCount = (str.match(/\p{Extended_Pictographic}/gu) || []).length
77
- return str.padEnd(width + emojiCount)
78
- }
79
-
80
73
  // Display manual installation instructions for missing dependencies
81
74
  function displayManualInstallInstructions(
82
75
  missingDeps: Array<{ dependency: { name: string }; installed: boolean }>,
@@ -482,13 +475,13 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
482
475
 
483
476
  // PostgreSQL rows
484
477
  for (const engine of pgEngines) {
485
- const icon = getEngineIcon(engine.engine)
486
478
  const platformInfo = `${engine.platform}-${engine.arch}`
487
- const engineDisplay = `${icon} ${engine.engine}`
479
+ // getEngineIcon() includes trailing space for consistent alignment
480
+ const engineDisplay = `${getEngineIcon(engine.engine)}${engine.engine}`
488
481
 
489
482
  console.log(
490
483
  chalk.gray(' ') +
491
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
484
+ chalk.cyan(engineDisplay.padEnd(14)) +
492
485
  chalk.yellow(engine.version.padEnd(12)) +
493
486
  chalk.gray(platformInfo.padEnd(18)) +
494
487
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -497,13 +490,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
497
490
 
498
491
  // MySQL rows
499
492
  for (const mysqlEngine of mysqlEngines) {
500
- const icon = ENGINE_ICONS.mysql
501
493
  const platformInfo = `${mysqlEngine.platform}-${mysqlEngine.arch}`
502
- const engineDisplay = `${icon} mysql`
494
+ const engineDisplay = `${getEngineIcon('mysql')}mysql`
503
495
 
504
496
  console.log(
505
497
  chalk.gray(' ') +
506
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
498
+ chalk.cyan(engineDisplay.padEnd(14)) +
507
499
  chalk.yellow(mysqlEngine.version.padEnd(12)) +
508
500
  chalk.gray(platformInfo.padEnd(18)) +
509
501
  chalk.white(formatBytes(mysqlEngine.sizeBytes)),
@@ -512,12 +504,11 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
512
504
 
513
505
  // SQLite row
514
506
  if (sqliteEngine) {
515
- const icon = ENGINE_ICONS.sqlite
516
- const engineDisplay = `${icon} sqlite`
507
+ const engineDisplay = `${getEngineIcon('sqlite')}sqlite`
517
508
 
518
509
  console.log(
519
510
  chalk.gray(' ') +
520
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
511
+ chalk.cyan(engineDisplay.padEnd(14)) +
521
512
  chalk.yellow(sqliteEngine.version.padEnd(12)) +
522
513
  chalk.gray('system'.padEnd(18)) +
523
514
  chalk.gray('(system-installed)'),
@@ -526,13 +517,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
526
517
 
527
518
  // DuckDB rows
528
519
  for (const engine of duckdbEngines) {
529
- const icon = ENGINE_ICONS.duckdb
530
520
  const platformInfo = `${engine.platform}-${engine.arch}`
531
- const engineDisplay = `${icon} duckdb`
521
+ const engineDisplay = `${getEngineIcon('duckdb')}duckdb`
532
522
 
533
523
  console.log(
534
524
  chalk.gray(' ') +
535
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
525
+ chalk.cyan(engineDisplay.padEnd(14)) +
536
526
  chalk.yellow(engine.version.padEnd(12)) +
537
527
  chalk.gray(platformInfo.padEnd(18)) +
538
528
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -541,13 +531,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
541
531
 
542
532
  // MongoDB rows
543
533
  for (const engine of mongodbEngines) {
544
- const icon = ENGINE_ICONS.mongodb
545
534
  const platformInfo = `${engine.platform}-${engine.arch}`
546
- const engineDisplay = `${icon} mongodb`
535
+ const engineDisplay = `${getEngineIcon('mongodb')}mongodb`
547
536
 
548
537
  console.log(
549
538
  chalk.gray(' ') +
550
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
539
+ chalk.cyan(engineDisplay.padEnd(14)) +
551
540
  chalk.yellow(engine.version.padEnd(12)) +
552
541
  chalk.gray(platformInfo.padEnd(18)) +
553
542
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -556,13 +545,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
556
545
 
557
546
  // FerretDB rows
558
547
  for (const engine of ferretdbEngines) {
559
- const icon = ENGINE_ICONS.ferretdb
560
548
  const platformInfo = `${engine.platform}-${engine.arch}`
561
- const engineDisplay = `${icon} ferretdb`
549
+ const engineDisplay = `${getEngineIcon('ferretdb')}ferretdb`
562
550
 
563
551
  console.log(
564
552
  chalk.gray(' ') +
565
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
553
+ chalk.cyan(engineDisplay.padEnd(14)) +
566
554
  chalk.yellow(engine.version.padEnd(12)) +
567
555
  chalk.gray(platformInfo.padEnd(18)) +
568
556
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -571,13 +559,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
571
559
 
572
560
  // Redis rows
573
561
  for (const engine of redisEngines) {
574
- const icon = ENGINE_ICONS.redis
575
562
  const platformInfo = `${engine.platform}-${engine.arch}`
576
- const engineDisplay = `${icon} redis`
563
+ const engineDisplay = `${getEngineIcon('redis')}redis`
577
564
 
578
565
  console.log(
579
566
  chalk.gray(' ') +
580
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
567
+ chalk.cyan(engineDisplay.padEnd(14)) +
581
568
  chalk.yellow(engine.version.padEnd(12)) +
582
569
  chalk.gray(platformInfo.padEnd(18)) +
583
570
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -586,13 +573,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
586
573
 
587
574
  // Valkey rows
588
575
  for (const engine of valkeyEngines) {
589
- const icon = ENGINE_ICONS.valkey
590
576
  const platformInfo = `${engine.platform}-${engine.arch}`
591
- const engineDisplay = `${icon} valkey`
577
+ const engineDisplay = `${getEngineIcon('valkey')}valkey`
592
578
 
593
579
  console.log(
594
580
  chalk.gray(' ') +
595
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
581
+ chalk.cyan(engineDisplay.padEnd(14)) +
596
582
  chalk.yellow(engine.version.padEnd(12)) +
597
583
  chalk.gray(platformInfo.padEnd(18)) +
598
584
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -601,13 +587,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
601
587
 
602
588
  // Qdrant rows
603
589
  for (const engine of qdrantEngines) {
604
- const icon = ENGINE_ICONS.qdrant
605
590
  const platformInfo = `${engine.platform}-${engine.arch}`
606
- const engineDisplay = `${icon} qdrant`
591
+ const engineDisplay = `${getEngineIcon('qdrant')}qdrant`
607
592
 
608
593
  console.log(
609
594
  chalk.gray(' ') +
610
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
595
+ chalk.cyan(engineDisplay.padEnd(14)) +
611
596
  chalk.yellow(engine.version.padEnd(12)) +
612
597
  chalk.gray(platformInfo.padEnd(18)) +
613
598
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -616,13 +601,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
616
601
 
617
602
  // Meilisearch rows
618
603
  for (const engine of meilisearchEngines) {
619
- const icon = ENGINE_ICONS.meilisearch
620
604
  const platformInfo = `${engine.platform}-${engine.arch}`
621
- const engineDisplay = `${icon} meilisearch`
605
+ const engineDisplay = `${getEngineIcon('meilisearch')}meilisearch`
622
606
 
623
607
  console.log(
624
608
  chalk.gray(' ') +
625
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
609
+ chalk.cyan(engineDisplay.padEnd(14)) +
626
610
  chalk.yellow(engine.version.padEnd(12)) +
627
611
  chalk.gray(platformInfo.padEnd(18)) +
628
612
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -631,13 +615,12 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
631
615
 
632
616
  // CouchDB rows
633
617
  for (const engine of couchdbEngines) {
634
- const icon = ENGINE_ICONS.couchdb
635
618
  const platformInfo = `${engine.platform}-${engine.arch}`
636
- const engineDisplay = `${icon} couchdb`
619
+ const engineDisplay = `${getEngineIcon('couchdb')}couchdb`
637
620
 
638
621
  console.log(
639
622
  chalk.gray(' ') +
640
- chalk.cyan(padWithEmoji(engineDisplay, 13)) +
623
+ chalk.cyan(engineDisplay.padEnd(14)) +
641
624
  chalk.yellow(engine.version.padEnd(12)) +
642
625
  chalk.gray(platformInfo.padEnd(18)) +
643
626
  chalk.white(formatBytes(engine.sizeBytes)),
@@ -778,7 +761,7 @@ async function deleteEngine(
778
761
  // Interactive selection if not provided
779
762
  if (!engineName || !engineVersion) {
780
763
  const choices = pgEngines.map((e) => ({
781
- 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)})`)}`,
782
765
  value: `${e.engine}:${e.version}:${e.path}`,
783
766
  }))
784
767
 
@@ -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)) +
@@ -347,7 +347,7 @@ export async function handleRestore(): Promise<void> {
347
347
 
348
348
  const choices = [
349
349
  ...running.map((c) => ({
350
- 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')}`,
351
351
  value: c.name,
352
352
  short: c.name,
353
353
  })),
@@ -442,7 +442,7 @@ export async function handleRestore(): Promise<void> {
442
442
  ]
443
443
 
444
444
  restoreChoices.push({
445
- name: `${chalk.cyan('🔗')} Connection string (pull from remote database)`,
445
+ name: `${chalk.cyan('↗')} Connection string ${chalk.gray('(pull from remote database)')}`,
446
446
  value: 'connection',
447
447
  })
448
448
 
@@ -1118,7 +1118,7 @@ export async function handleRestoreForContainer(
1118
1118
  value: 'file',
1119
1119
  },
1120
1120
  {
1121
- name: `${chalk.cyan('🔗')} Connection string (pull from remote database)`,
1121
+ name: `${chalk.cyan('↗')} Connection string ${chalk.gray('(pull from remote database)')}`,
1122
1122
  value: 'connection',
1123
1123
  },
1124
1124
  ]
@@ -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,38 +662,29 @@ 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
690
  name: `${chalk.magenta('⊕')} Copy connection string`,
@@ -697,31 +693,21 @@ export async function showContainerSubmenu(
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,
@@ -93,7 +98,7 @@ export async function handleEngines(): Promise<void> {
93
98
  const COL_SIZE = 10
94
99
 
95
100
  // Build selectable choices with formatted display
96
- const choices: MenuChoice[] = allEnginesSorted.map((e) => {
101
+ const engineChoices: FilterableChoice[] = allEnginesSorted.map((e) => {
97
102
  // Use getEngineIconPadded to handle emoji width inconsistencies
98
103
  // Icons like ðŸĶ­ and ðŸŠķ render at width 1, others at width 2
99
104
  const icon = getEngineIconPadded(e.engine)
@@ -114,25 +119,26 @@ export async function handleEngines(): Promise<void> {
114
119
  }
115
120
  })
116
121
 
117
- choices.push(new inquirer.Separator(chalk.gray('─'.repeat(52))))
118
- 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))),
119
126
  new inquirer.Separator(
120
- chalk.gray(`Total: ${engines.length} engine(s), ${formatBytes(totalSize)}`),
127
+ `Total: ${engines.length} engine(s), ${formatBytes(totalSize)} ${chalk.gray('— type to filter')}`,
121
128
  ),
122
- )
123
- choices.push(new inquirer.Separator())
124
- choices.push({ name: `${chalk.blue('←')} Back to main menu ${chalk.gray('(esc)')}`, value: 'back' })
125
- 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
+ ]
126
132
 
127
- const { action } = await escapeablePrompt<{ action: string }>([
133
+ const action = await filterableListPrompt(
134
+ allChoices,
135
+ 'Select an engine:',
128
136
  {
129
- type: 'list',
130
- name: 'action',
131
- message: 'Select an engine:',
132
- choices,
137
+ filterableCount: engineChoices.length,
133
138
  pageSize: 18,
139
+ emptyText: 'No engines match filter',
134
140
  },
135
- ])
141
+ )
136
142
 
137
143
  // Back returns to main menu (escape is handled globally)
138
144
  if (action === 'back') {
@@ -176,7 +182,7 @@ async function showEngineSubmenu(
176
182
  console.log()
177
183
  console.log(
178
184
  chalk.cyan(
179
- ` ${getEngineIcon(engineName)} ${engineName} ${engineVersion} ${chalk.gray(`(${formatBytes(sizeBytes)})`)}`,
185
+ ` ${getEngineIcon(engineName)}${engineName} ${engineVersion} ${chalk.gray(`(${formatBytes(sizeBytes)})`)}`,
180
186
  ),
181
187
  )
182
188
  console.log()