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
|
|
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
|
-
|
|
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
|
-
//
|
|
618
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
let
|
|
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
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
|
+
"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:
|
|
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"
|