spindb 0.31.3 → 0.31.4

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.
@@ -444,6 +444,7 @@ export async function handleCreate(): Promise<'main' | string | void> {
444
444
 
445
445
  export async function handleList(
446
446
  showMainMenu: () => Promise<void>,
447
+ options?: { focusContainer?: string },
447
448
  ): Promise<void> {
448
449
  console.clear()
449
450
  console.log(header('Containers'))
@@ -570,19 +571,19 @@ export async function handleList(
570
571
  (c) => !isFileBasedEngine(c.engine),
571
572
  )
572
573
 
573
- // Build the full choice list with header hint and footer items
574
+ // Build the full choice list with footer items
575
+ // IMPORTANT: Containers must come FIRST because filterableCount slices from index 0
574
576
  const summary = `${containers.length} container(s): ${parts.join('; ')}`
575
577
  const allChoices: (FilterableChoice | inquirer.Separator)[] = [
576
- // Show toggle hint at top when server-based containers exist
578
+ ...containerChoices,
579
+ // Show toggle hint after containers (before footer) when server-based containers exist
577
580
  ...(hasServerContainers
578
581
  ? [
579
582
  new inquirer.Separator(
580
583
  chalk.cyan('── [Shift+Tab] toggle start/stop ──'),
581
584
  ),
582
585
  ]
583
- : []),
584
- ...containerChoices,
585
- new inquirer.Separator(),
586
+ : [new inquirer.Separator()]),
586
587
  new inquirer.Separator(summary),
587
588
  new inquirer.Separator(),
588
589
  { name: `${chalk.green('+')} Create new`, value: 'create' },
@@ -601,6 +602,7 @@ export async function handleList(
601
602
  pageSize: getPageSize(),
602
603
  emptyText: 'No containers match filter',
603
604
  enableToggle: hasServerContainers,
605
+ defaultValue: options?.focusContainer,
604
606
  },
605
607
  )
606
608
 
@@ -614,27 +616,21 @@ export async function handleList(
614
616
  engine: config.engine,
615
617
  })
616
618
 
617
- // Clear screen and show brief status
618
- console.clear()
619
- console.log(header('Containers'))
620
-
619
+ // Show inline status without clearing screen
620
+ console.log()
621
621
  if (isRunning) {
622
622
  await handleStopContainer(containerName)
623
- // Brief pause so user can see the result
624
- await new Promise((resolve) => setTimeout(resolve, 300))
625
623
  } else {
626
624
  const result = await handleStartContainer(containerName)
627
625
  if (result === 'home') {
628
626
  await showMainMenu()
629
627
  return
630
628
  }
631
- // Brief pause so user can see the result (for 'started' or 'back')
632
- await new Promise((resolve) => setTimeout(resolve, 300))
633
629
  }
634
630
  }
635
631
 
636
- // Refresh the container list
637
- await handleList(showMainMenu)
632
+ // Refresh the container list with cursor on the same container
633
+ await handleList(showMainMenu, { focusContainer: containerName })
638
634
  return
639
635
  }
640
636
 
package/cli/ui/prompts.ts CHANGED
@@ -41,14 +41,14 @@ export const TOGGLE_PREFIX = '__toggle__:'
41
41
  let globalEscapeEnabled = false
42
42
  let escapeTriggered = false
43
43
  let escapeReject: ((error: Error) => void) | null = null
44
- let currentPromptUi: { close?: () => void } | null = null
44
+ // Store the raw UI object so we can access activePrompt dynamically
45
+ // (activePrompt may not be set at capture time due to async initialization)
46
+ let currentPromptUi: Record<string, unknown> | null = null
45
47
 
46
48
  // Toggle handler state (Shift+Tab to toggle container start/stop)
47
49
  let toggleEnabled = false
48
- let toggleCursorPosition = 0
49
- let toggleCurrentItems: FilterableChoice[] = []
50
- let toggleTriggered = false
51
- let toggleTargetValue: string | null = null
50
+ // Set of values that are valid toggle targets (container names)
51
+ let toggleValidTargets: Set<string> = new Set()
52
52
 
53
53
  // Custom error class for escape
54
54
  export class EscapeError extends Error {
@@ -82,45 +82,61 @@ function onEscapeData(data: Buffer): void {
82
82
  if (data.length === 3 && data[0] === 27 && data[1] === 91) {
83
83
  const keyCode = data[2]
84
84
 
85
- // Arrow Up: \x1b[A (27, 91, 65)
86
- if (keyCode === 65 && toggleEnabled) {
87
- toggleCursorPosition = Math.max(0, toggleCursorPosition - 1)
88
- return // Let inquirer handle the actual cursor movement
89
- }
85
+ // Shift+Tab: \x1b[Z (27, 91, 90)
86
+ // Access inquirer's internal state to get the currently highlighted value
87
+ if (keyCode === 90 && toggleEnabled && currentPromptUi) {
88
+ try {
89
+ // Access activePrompt dynamically (it may not be set at capture time)
90
+ // inquirer-autocomplete-prompt uses 'selected' for cursor index
91
+ // and currentChoices.getChoice(index) to get the choice object
92
+ const activePrompt = currentPromptUi.activePrompt as
93
+ | {
94
+ selected?: number
95
+ currentChoices?: {
96
+ getChoice?: (
97
+ index: number,
98
+ ) => { value?: string; type?: string } | undefined
99
+ }
100
+ }
101
+ | undefined
90
102
 
91
- // Arrow Down: \x1b[B (27, 91, 66)
92
- if (keyCode === 66 && toggleEnabled) {
93
- toggleCursorPosition = Math.min(
94
- toggleCurrentItems.length - 1,
95
- toggleCursorPosition + 1,
96
- )
97
- return // Let inquirer handle the actual cursor movement
98
- }
103
+ if (
104
+ activePrompt?.currentChoices?.getChoice &&
105
+ activePrompt.selected !== undefined
106
+ ) {
107
+ const currentChoice = activePrompt.currentChoices.getChoice(
108
+ activePrompt.selected,
109
+ )
99
110
 
100
- // Shift+Tab: \x1b[Z (27, 91, 90)
101
- if (keyCode === 90 && toggleEnabled && toggleCurrentItems.length > 0) {
102
- // Get the currently highlighted item's value
103
- const currentItem = toggleCurrentItems[toggleCursorPosition]
104
- if (currentItem) {
105
- toggleTriggered = true
106
- toggleTargetValue = currentItem.value
107
-
108
- // Reject the prompt to interrupt it
109
- if (escapeReject) {
110
- const reject = escapeReject
111
- escapeReject = null
112
- reject(new ToggleError(currentItem.value))
113
- }
111
+ // Check if the highlighted item is a valid toggle target (a container)
112
+ // Skip separators (type === 'separator') and non-container items
113
+ if (
114
+ currentChoice &&
115
+ currentChoice.value &&
116
+ currentChoice.type !== 'separator' &&
117
+ toggleValidTargets.has(currentChoice.value)
118
+ ) {
119
+ // Reject the prompt with the container value to toggle
120
+ if (escapeReject) {
121
+ const reject = escapeReject
122
+ escapeReject = null
123
+ reject(new ToggleError(currentChoice.value))
124
+ }
114
125
 
115
- // Close the prompt UI
116
- if (currentPromptUi?.close) {
117
- try {
118
- currentPromptUi.close()
119
- } catch {
120
- // Swallow errors from inquirer internals
126
+ // Close the prompt UI
127
+ if (typeof currentPromptUi.close === 'function') {
128
+ try {
129
+ currentPromptUi.close()
130
+ } catch {
131
+ // Swallow errors from inquirer internals
132
+ }
133
+ currentPromptUi = null
134
+ }
121
135
  }
122
- currentPromptUi = null
123
136
  }
137
+ } catch {
138
+ // Ignore errors accessing inquirer internals - the prompt state
139
+ // may be inconsistent during filtering or other operations
124
140
  }
125
141
  return
126
142
  }
@@ -141,7 +157,7 @@ function onEscapeData(data: Buffer): void {
141
157
  // Then close the prompt UI to stop it from rendering
142
158
  // Do this after rejecting so the error propagates first
143
159
  // Wrap in try/catch as inquirer internals may change between versions
144
- if (currentPromptUi?.close) {
160
+ if (currentPromptUi && typeof currentPromptUi.close === 'function') {
145
161
  try {
146
162
  currentPromptUi.close()
147
163
  } catch {
@@ -152,12 +168,6 @@ function onEscapeData(data: Buffer): void {
152
168
  // Clear the screen
153
169
  console.clear()
154
170
  }
155
-
156
- // Any other input (typing to filter) resets cursor to 0
157
- // This helps keep our tracking in sync when the list is filtered
158
- if (toggleEnabled && data.length > 0 && data[0] !== 27) {
159
- toggleCursorPosition = 0
160
- }
161
171
  }
162
172
 
163
173
  /**
@@ -196,14 +206,11 @@ export function checkAndResetEscape(): boolean {
196
206
 
197
207
  /**
198
208
  * Enable toggle tracking for the container list.
199
- * This tracks arrow key movements and Shift+Tab presses.
209
+ * @param validTargets - Set of values that are valid toggle targets (container names)
200
210
  */
201
- export function enableToggleTracking(): void {
211
+ export function enableToggleTracking(validTargets: Set<string>): void {
202
212
  toggleEnabled = true
203
- toggleCursorPosition = 0
204
- toggleCurrentItems = []
205
- toggleTriggered = false
206
- toggleTargetValue = null
213
+ toggleValidTargets = validTargets
207
214
  }
208
215
 
209
216
  /**
@@ -211,36 +218,7 @@ export function enableToggleTracking(): void {
211
218
  */
212
219
  export function disableToggleTracking(): void {
213
220
  toggleEnabled = false
214
- toggleCursorPosition = 0
215
- toggleCurrentItems = []
216
- toggleTriggered = false
217
- toggleTargetValue = null
218
- }
219
-
220
- /**
221
- * Update the current list of filterable items.
222
- * Called by filterableListPrompt when the source function returns new items.
223
- */
224
- export function updateToggleItems(items: FilterableChoice[]): void {
225
- toggleCurrentItems = items
226
- // Clamp cursor position to valid range
227
- if (toggleCursorPosition >= items.length) {
228
- toggleCursorPosition = Math.max(0, items.length - 1)
229
- }
230
- }
231
-
232
- /**
233
- * Check if toggle was triggered and get the target value.
234
- * Resets the toggle state after reading.
235
- */
236
- export function checkAndResetToggle(): {
237
- triggered: boolean
238
- value: string | null
239
- } {
240
- const result = { triggered: toggleTriggered, value: toggleTargetValue }
241
- toggleTriggered = false
242
- toggleTargetValue = null
243
- return result
221
+ toggleValidTargets = new Set()
244
222
  }
245
223
 
246
224
  /**
@@ -279,7 +257,7 @@ export async function escapeablePrompt<T extends Record<string, unknown>>(
279
257
  promptWithUi.ui !== null &&
280
258
  typeof (promptWithUi.ui as Record<string, unknown>).close === 'function'
281
259
  ) {
282
- currentPromptUi = promptWithUi.ui as { close: () => void }
260
+ currentPromptUi = promptWithUi.ui as Record<string, unknown>
283
261
  } else {
284
262
  currentPromptUi = null
285
263
  }
@@ -322,6 +300,7 @@ export async function filterableListPrompt(
322
300
  pageSize?: number
323
301
  emptyText?: string
324
302
  enableToggle?: boolean
303
+ defaultValue?: string // Pre-select this value (cursor starts here)
325
304
  },
326
305
  ): Promise<string> {
327
306
  // Split choices into filterable items and static footer (separators, back buttons, etc.)
@@ -332,9 +311,10 @@ export async function filterableListPrompt(
332
311
  const footerItems = choices.slice(options.filterableCount)
333
312
 
334
313
  // Enable toggle tracking if requested
314
+ // Build a set of valid toggle targets (the container values from filterable items)
335
315
  if (options.enableToggle) {
336
- enableToggleTracking()
337
- updateToggleItems(filterableItems)
316
+ const validTargets = new Set(filterableItems.map((item) => item.value))
317
+ enableToggleTracking(validTargets)
338
318
  }
339
319
 
340
320
  // Source function for autocomplete - filters items based on input
@@ -349,10 +329,6 @@ export async function filterableListPrompt(
349
329
  if (!searchTerm) {
350
330
  // No filter - show all items
351
331
  result = [...filterableItems, ...footerItems]
352
- // Update toggle tracking with current filterable items
353
- if (options.enableToggle) {
354
- updateToggleItems(filterableItems)
355
- }
356
332
  } else {
357
333
  // Filter items by matching search term against the display name
358
334
  // Strip ANSI codes for matching but keep them for display
@@ -372,16 +348,8 @@ export async function filterableListPrompt(
372
348
  ),
373
349
  ...footerItems,
374
350
  ]
375
- // Update toggle tracking with empty list (no items to toggle)
376
- if (options.enableToggle) {
377
- updateToggleItems([])
378
- }
379
351
  } else {
380
352
  result = [...filtered, ...footerItems]
381
- // Update toggle tracking with filtered items
382
- if (options.enableToggle) {
383
- updateToggleItems(filtered)
384
- }
385
353
  }
386
354
  }
387
355
 
@@ -406,10 +374,14 @@ export async function filterableListPrompt(
406
374
  // Suppress the default "(Use arrow keys or type to search)" suffix
407
375
  // since we include custom instructions in the message
408
376
  suffix: '',
377
+ // Pre-select a value (cursor starts on this item)
378
+ default: options.defaultValue,
409
379
  },
410
380
  ])
411
381
 
412
- // Register the prompt UI for escape handling
382
+ // Register the prompt UI for escape and toggle handling
383
+ // Store the raw UI object so we can access activePrompt dynamically
384
+ // (activePrompt contains selected and currentChoices for the highlighted item)
413
385
  const promptWithUi = p as unknown as Record<string, unknown>
414
386
  if (
415
387
  promptWithUi.ui &&
@@ -417,7 +389,7 @@ export async function filterableListPrompt(
417
389
  promptWithUi.ui !== null &&
418
390
  typeof (promptWithUi.ui as Record<string, unknown>).close === 'function'
419
391
  ) {
420
- currentPromptUi = promptWithUi.ui as { close: () => void }
392
+ currentPromptUi = promptWithUi.ui as Record<string, unknown>
421
393
  } else {
422
394
  currentPromptUi = null
423
395
  }
@@ -623,6 +623,9 @@ export class FerretDBEngine extends BaseEngine {
623
623
  const spawnOpts: SpawnOptions = {
624
624
  stdio: ['ignore', 'pipe', 'pipe'],
625
625
  detached: true,
626
+ // Run FerretDB in the container directory so telemetry.json/state.json
627
+ // are written there instead of polluting the user's cwd
628
+ cwd: containerDir,
626
629
  }
627
630
 
628
631
  const proc = spawn(ferretdbBinary, ferretArgs, spawnOpts)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.31.3",
3
+ "version": "0.31.4",
4
4
  "author": "Bob Bass <bob@bbass.co>",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
@@ -22,7 +22,10 @@
22
22
  "test:local:quick": "./scripts/test-local.sh --quick",
23
23
  "test:local:fresh": "./scripts/test-local.sh --fresh",
24
24
  "test:docker": "bash tests/docker/run-docker-test.sh",
25
- "generate:qdrant-snapshot": "tsx scripts/generate-qdrant-snapshot.ts",
25
+ "generate:backup": "tsx scripts/generate/backup/index.ts",
26
+ "generate:db": "tsx scripts/generate/db/index.ts",
27
+ "generate:missing": "tsx scripts/generate/missing-databases.ts",
28
+ "delete:demos": "tsx scripts/generate/delete-demos.ts",
26
29
  "format": "prettier --write .",
27
30
  "lint": "tsc --noEmit && eslint .",
28
31
  "prepare": "husky"