@crustjs/prompts 0.0.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +966 -0
  2. package/dist/index.js +1002 -0
  3. package/package.json +53 -0
package/dist/index.js ADDED
@@ -0,0 +1,1002 @@
1
+ // @bun
2
+ // src/core/theme.ts
3
+ import { bold, cyan, dim, green, magenta, red, yellow } from "@crustjs/style";
4
+ var defaultTheme = {
5
+ prefix: cyan,
6
+ message: bold,
7
+ placeholder: dim,
8
+ cursor: cyan,
9
+ selected: yellow,
10
+ unselected: dim,
11
+ error: red,
12
+ success: green,
13
+ hint: dim,
14
+ spinner: magenta,
15
+ filterMatch: cyan
16
+ };
17
+ function createTheme(overrides) {
18
+ if (!overrides)
19
+ return defaultTheme;
20
+ return { ...defaultTheme, ...overrides };
21
+ }
22
+ var globalOverrides;
23
+ function setTheme(theme) {
24
+ globalOverrides = theme;
25
+ }
26
+ function getTheme() {
27
+ if (!globalOverrides)
28
+ return defaultTheme;
29
+ return { ...defaultTheme, ...globalOverrides };
30
+ }
31
+ function resolveTheme(promptTheme) {
32
+ if (!globalOverrides && !promptTheme)
33
+ return defaultTheme;
34
+ return {
35
+ ...defaultTheme,
36
+ ...globalOverrides,
37
+ ...promptTheme
38
+ };
39
+ }
40
+ // src/core/renderer.ts
41
+ import * as readline from "readline";
42
+ var SUBMIT = Symbol("submit");
43
+ function submit(value) {
44
+ return { [SUBMIT]: value };
45
+ }
46
+ var promptActive = false;
47
+ var ESC = "\x1B[";
48
+ var HIDE_CURSOR = `${ESC}?25l`;
49
+ var SHOW_CURSOR = `${ESC}?25h`;
50
+ var ERASE_LINE = `${ESC}2K`;
51
+ function cursorUp(n) {
52
+ return n > 0 ? `${ESC}${n}A` : "";
53
+ }
54
+
55
+ class NonInteractiveError extends Error {
56
+ constructor(message) {
57
+ super(message ?? "Prompts require an interactive terminal (TTY). Provide an `initial` value for non-interactive environments.");
58
+ this.name = "NonInteractiveError";
59
+ }
60
+ }
61
+
62
+ class CancelledError extends Error {
63
+ constructor(message) {
64
+ super(message ?? "Prompt was cancelled.");
65
+ this.name = "CancelledError";
66
+ }
67
+ }
68
+ function assertTTY() {
69
+ if (!process.stdin.isTTY) {
70
+ throw new NonInteractiveError;
71
+ }
72
+ }
73
+ function isSubmit(result) {
74
+ return typeof result === "object" && result !== null && SUBMIT in result;
75
+ }
76
+ function runPrompt(config) {
77
+ const { render, handleKey, initialState, theme, renderSubmitted } = config;
78
+ return new Promise((resolve, reject) => {
79
+ if (promptActive) {
80
+ reject(new Error("Cannot run multiple prompts concurrently. Await each prompt before starting the next."));
81
+ return;
82
+ }
83
+ try {
84
+ assertTTY();
85
+ } catch (err) {
86
+ reject(err);
87
+ return;
88
+ }
89
+ promptActive = true;
90
+ let state = initialState;
91
+ let prevLineCount = 0;
92
+ let isCleanedUp = false;
93
+ const stdin = process.stdin;
94
+ const output = process.stderr;
95
+ function cleanup() {
96
+ if (isCleanedUp)
97
+ return;
98
+ isCleanedUp = true;
99
+ promptActive = false;
100
+ stdin.removeListener("keypress", onKeypress);
101
+ if (stdin.isTTY && stdin.isRaw) {
102
+ stdin.setRawMode(false);
103
+ }
104
+ stdin.pause();
105
+ output.write(SHOW_CURSOR);
106
+ }
107
+ function renderFrame(content) {
108
+ if (prevLineCount > 0) {
109
+ output.write(`${cursorUp(prevLineCount - 1)}\r`);
110
+ for (let i = 0;i < prevLineCount; i++) {
111
+ output.write(ERASE_LINE);
112
+ if (i < prevLineCount - 1) {
113
+ output.write(`${ESC}1B`);
114
+ }
115
+ }
116
+ if (prevLineCount > 1) {
117
+ output.write(cursorUp(prevLineCount - 1));
118
+ }
119
+ output.write(`\r`);
120
+ }
121
+ output.write(content);
122
+ const lines = content.split(`
123
+ `);
124
+ prevLineCount = lines.length;
125
+ }
126
+ async function onKeypress(ch, key) {
127
+ if (key?.ctrl && key.name === "c") {
128
+ cleanup();
129
+ reject(new CancelledError);
130
+ return;
131
+ }
132
+ const event = {
133
+ char: ch ?? "",
134
+ name: key?.name ?? "",
135
+ ctrl: key?.ctrl ?? false,
136
+ meta: key?.meta ?? false,
137
+ shift: key?.shift ?? false
138
+ };
139
+ try {
140
+ const result = await handleKey(event, state);
141
+ if (isSubmit(result)) {
142
+ const value = result[SUBMIT];
143
+ if (renderSubmitted) {
144
+ renderFrame(renderSubmitted(state, value, theme));
145
+ }
146
+ output.write(`
147
+ `);
148
+ cleanup();
149
+ resolve(value);
150
+ } else {
151
+ state = result;
152
+ renderFrame(render(state, theme));
153
+ }
154
+ } catch (err) {
155
+ cleanup();
156
+ reject(err);
157
+ }
158
+ }
159
+ try {
160
+ readline.emitKeypressEvents(stdin);
161
+ stdin.setRawMode(true);
162
+ stdin.resume();
163
+ output.write(HIDE_CURSOR);
164
+ const initialContent = render(state, theme);
165
+ output.write(initialContent);
166
+ const lines = initialContent.split(`
167
+ `);
168
+ prevLineCount = lines.length;
169
+ stdin.on("keypress", onKeypress);
170
+ } catch (err) {
171
+ cleanup();
172
+ reject(err);
173
+ }
174
+ });
175
+ }
176
+ // src/prompts/confirm.ts
177
+ function handleKey(key, state) {
178
+ if (key.name === "return") {
179
+ return submit(state.value);
180
+ }
181
+ if (key.name === "left" || key.name === "right") {
182
+ return { value: !state.value };
183
+ }
184
+ if (key.name === "h") {
185
+ return { value: true };
186
+ }
187
+ if (key.name === "l") {
188
+ return { value: false };
189
+ }
190
+ if (key.name === "tab") {
191
+ return { value: !state.value };
192
+ }
193
+ if (key.char === "y" || key.char === "Y") {
194
+ return { value: true };
195
+ }
196
+ if (key.char === "n" || key.char === "N") {
197
+ return { value: false };
198
+ }
199
+ return state;
200
+ }
201
+ var PREFIX_SYMBOL = "?";
202
+ var SEPARATOR = " / ";
203
+ function renderConfirm(state, theme, message, activeLabel, inactiveLabel) {
204
+ const prefix = theme.prefix(PREFIX_SYMBOL);
205
+ const msg = theme.message(message);
206
+ const activeDisplay = state.value ? theme.selected(activeLabel) : theme.unselected(activeLabel);
207
+ const inactiveDisplay = state.value ? theme.unselected(inactiveLabel) : theme.selected(inactiveLabel);
208
+ return `${prefix} ${msg}
209
+ ${activeDisplay}${SEPARATOR}${inactiveDisplay}`;
210
+ }
211
+ function renderSubmitted(_state, value, theme, message, activeLabel, inactiveLabel) {
212
+ const prefix = theme.prefix(PREFIX_SYMBOL);
213
+ const msg = theme.message(message);
214
+ const answer = value ? activeLabel : inactiveLabel;
215
+ return `${prefix} ${msg} ${theme.success(answer)}`;
216
+ }
217
+ async function confirm(options) {
218
+ if (options.initial !== undefined) {
219
+ return options.initial;
220
+ }
221
+ const theme = resolveTheme(options.theme);
222
+ const activeLabel = options.active ?? "Yes";
223
+ const inactiveLabel = options.inactive ?? "No";
224
+ const defaultValue = options.default ?? true;
225
+ const initialState = {
226
+ value: defaultValue
227
+ };
228
+ return runPrompt({
229
+ initialState,
230
+ theme,
231
+ render: (state, t) => renderConfirm(state, t, options.message, activeLabel, inactiveLabel),
232
+ handleKey,
233
+ renderSubmitted: (state, value, t) => renderSubmitted(state, value, t, options.message, activeLabel, inactiveLabel)
234
+ });
235
+ }
236
+ // src/core/fuzzy.ts
237
+ var CONSECUTIVE_BONUS = 5;
238
+ var START_BONUS = 10;
239
+ var WORD_BOUNDARY_BONUS = 8;
240
+ var MATCH_SCORE = 1;
241
+ function fuzzyMatch(query, candidate) {
242
+ if (query.length === 0) {
243
+ return { match: true, score: 0, indices: [] };
244
+ }
245
+ const queryLower = query.toLowerCase();
246
+ const candidateLower = candidate.toLowerCase();
247
+ const indices = [];
248
+ let score = 0;
249
+ let queryIdx = 0;
250
+ let prevMatchIdx = -2;
251
+ for (let candIdx = 0;candIdx < candidateLower.length && queryIdx < queryLower.length; candIdx++) {
252
+ if (candidateLower[candIdx] === queryLower[queryIdx]) {
253
+ indices.push(candIdx);
254
+ score += MATCH_SCORE;
255
+ if (candIdx === prevMatchIdx + 1) {
256
+ score += CONSECUTIVE_BONUS;
257
+ }
258
+ if (candIdx === 0) {
259
+ score += START_BONUS;
260
+ }
261
+ if (candIdx > 0) {
262
+ const prevChar = candidate[candIdx - 1];
263
+ if (prevChar === " " || prevChar === "-" || prevChar === "_" || prevChar === ".") {
264
+ score += WORD_BOUNDARY_BONUS;
265
+ }
266
+ }
267
+ prevMatchIdx = candIdx;
268
+ queryIdx++;
269
+ }
270
+ }
271
+ if (queryIdx < queryLower.length) {
272
+ return { match: false, score: 0, indices: [] };
273
+ }
274
+ return { match: true, score, indices };
275
+ }
276
+ function fuzzyFilter(query, items) {
277
+ if (query.length === 0) {
278
+ return items.map((item) => ({ item, score: 0, indices: [] }));
279
+ }
280
+ const results = [];
281
+ for (const item of items) {
282
+ const result = fuzzyMatch(query, item.label);
283
+ if (result.match) {
284
+ results.push({
285
+ item,
286
+ score: result.score,
287
+ indices: result.indices
288
+ });
289
+ }
290
+ }
291
+ results.sort((a, b) => b.score - a.score);
292
+ return results;
293
+ }
294
+
295
+ // src/core/textEdit.ts
296
+ var CURSOR_CHAR = "\u2502";
297
+ function handleTextEdit(key, text, cursorPos) {
298
+ if (key.name === "backspace") {
299
+ if (cursorPos === 0)
300
+ return { text, cursorPos };
301
+ const before = text.slice(0, cursorPos - 1);
302
+ const after = text.slice(cursorPos);
303
+ return { text: before + after, cursorPos: cursorPos - 1 };
304
+ }
305
+ if (key.name === "delete") {
306
+ if (cursorPos >= text.length)
307
+ return { text, cursorPos };
308
+ const before = text.slice(0, cursorPos);
309
+ const after = text.slice(cursorPos + 1);
310
+ return { text: before + after, cursorPos };
311
+ }
312
+ if (key.name === "left") {
313
+ if (cursorPos === 0)
314
+ return { text, cursorPos };
315
+ return { text, cursorPos: cursorPos - 1 };
316
+ }
317
+ if (key.name === "right") {
318
+ if (cursorPos >= text.length)
319
+ return { text, cursorPos };
320
+ return { text, cursorPos: cursorPos + 1 };
321
+ }
322
+ if (key.name === "home") {
323
+ return { text, cursorPos: 0 };
324
+ }
325
+ if (key.name === "end") {
326
+ return { text, cursorPos: text.length };
327
+ }
328
+ if (key.char.length === 1 && !key.ctrl && !key.meta) {
329
+ const before = text.slice(0, cursorPos);
330
+ const after = text.slice(cursorPos);
331
+ return { text: before + key.char + after, cursorPos: cursorPos + 1 };
332
+ }
333
+ return null;
334
+ }
335
+
336
+ // src/core/utils.ts
337
+ function calculateScrollOffset(cursor, scrollOffset, totalItems, maxVisible) {
338
+ const visibleCount = Math.min(totalItems, maxVisible);
339
+ if (cursor < scrollOffset) {
340
+ return cursor;
341
+ }
342
+ if (cursor >= scrollOffset + visibleCount) {
343
+ return cursor - visibleCount + 1;
344
+ }
345
+ return scrollOffset;
346
+ }
347
+ function normalizeChoices(choices) {
348
+ return choices.map((choice) => {
349
+ if (typeof choice === "string") {
350
+ return { label: choice, value: choice };
351
+ }
352
+ return choice;
353
+ });
354
+ }
355
+
356
+ // src/prompts/filter.ts
357
+ var DEFAULT_MAX_VISIBLE = 10;
358
+ var PREFIX_SYMBOL2 = "?";
359
+ var LIST_CURSOR_INDICATOR = ">";
360
+ var SCROLL_UP_INDICATOR = "...";
361
+ var SCROLL_DOWN_INDICATOR = "...";
362
+ function refilter(state, maxVisible) {
363
+ const results = fuzzyFilter(state.query, state.choices);
364
+ const listCursor = 0;
365
+ const scrollOffset = calculateScrollOffset(listCursor, 0, results.length, maxVisible);
366
+ return { ...state, results, listCursor, scrollOffset };
367
+ }
368
+ function createHandleKey(maxVisible) {
369
+ return (key, state) => {
370
+ if (key.name === "return") {
371
+ const result = state.results[state.listCursor];
372
+ if (result) {
373
+ return submit(result.item.value);
374
+ }
375
+ return state;
376
+ }
377
+ if (key.name === "up") {
378
+ if (state.results.length === 0)
379
+ return state;
380
+ const totalItems = state.results.length;
381
+ const newCursor = state.listCursor <= 0 ? totalItems - 1 : state.listCursor - 1;
382
+ const newScrollOffset = calculateScrollOffset(newCursor, state.scrollOffset, totalItems, maxVisible);
383
+ return {
384
+ ...state,
385
+ listCursor: newCursor,
386
+ scrollOffset: newScrollOffset
387
+ };
388
+ }
389
+ if (key.name === "down") {
390
+ if (state.results.length === 0)
391
+ return state;
392
+ const totalItems = state.results.length;
393
+ const newCursor = state.listCursor >= totalItems - 1 ? 0 : state.listCursor + 1;
394
+ const newScrollOffset = calculateScrollOffset(newCursor, state.scrollOffset, totalItems, maxVisible);
395
+ return {
396
+ ...state,
397
+ listCursor: newCursor,
398
+ scrollOffset: newScrollOffset
399
+ };
400
+ }
401
+ const edit = handleTextEdit(key, state.query, state.cursorPos);
402
+ if (edit) {
403
+ const queryChanged = edit.text !== state.query;
404
+ const newState = {
405
+ ...state,
406
+ query: edit.text,
407
+ cursorPos: edit.cursorPos
408
+ };
409
+ return queryChanged ? refilter(newState, maxVisible) : newState;
410
+ }
411
+ return state;
412
+ };
413
+ }
414
+ function highlightMatches(label, indices, theme) {
415
+ if (indices.length === 0)
416
+ return label;
417
+ const indexSet = new Set(indices);
418
+ let result = "";
419
+ let i = 0;
420
+ while (i < label.length) {
421
+ if (indexSet.has(i)) {
422
+ let matchedChars = "";
423
+ while (i < label.length && indexSet.has(i)) {
424
+ matchedChars += label[i];
425
+ i++;
426
+ }
427
+ result += theme.filterMatch(matchedChars);
428
+ } else {
429
+ result += label[i];
430
+ i++;
431
+ }
432
+ }
433
+ return result;
434
+ }
435
+ function renderFilter(state, theme, message, placeholder, maxVisible) {
436
+ const prefix = theme.prefix(PREFIX_SYMBOL2);
437
+ const msg = theme.message(message);
438
+ let queryLine;
439
+ if (state.query === "") {
440
+ if (placeholder) {
441
+ queryLine = theme.placeholder(placeholder);
442
+ } else {
443
+ queryLine = theme.cursor(CURSOR_CHAR);
444
+ }
445
+ } else {
446
+ const before = state.query.slice(0, state.cursorPos);
447
+ const after = state.query.slice(state.cursorPos);
448
+ queryLine = `${before}${theme.cursor(CURSOR_CHAR)}${after}`;
449
+ }
450
+ const lines = [`${prefix} ${msg}`, queryLine];
451
+ const totalResults = state.results.length;
452
+ if (totalResults === 0 && state.query.length > 0) {
453
+ lines.push(theme.hint("No matches"));
454
+ return lines.join(`
455
+ `);
456
+ }
457
+ const visibleCount = Math.min(totalResults, maxVisible);
458
+ const hasScrollUp = state.scrollOffset > 0;
459
+ if (hasScrollUp) {
460
+ lines.push(theme.hint(SCROLL_UP_INDICATOR));
461
+ }
462
+ for (let i = 0;i < visibleCount; i++) {
463
+ const resultIndex = state.scrollOffset + i;
464
+ const result = state.results[resultIndex];
465
+ if (!result)
466
+ break;
467
+ const isActive = resultIndex === state.listCursor;
468
+ const label = highlightMatches(result.item.label, result.indices, theme);
469
+ if (isActive) {
470
+ lines.push(`${theme.cursor(LIST_CURSOR_INDICATOR)} ${theme.selected(label)}`);
471
+ } else {
472
+ lines.push(` ${theme.unselected(label)}`);
473
+ }
474
+ }
475
+ const hasScrollDown = state.scrollOffset + visibleCount < totalResults;
476
+ if (hasScrollDown) {
477
+ lines.push(theme.hint(SCROLL_DOWN_INDICATOR));
478
+ }
479
+ return lines.join(`
480
+ `);
481
+ }
482
+ function renderSubmitted2(_state, _value, theme, message, results, listCursor) {
483
+ const prefix = theme.prefix(PREFIX_SYMBOL2);
484
+ const msg = theme.message(message);
485
+ const selected = results[listCursor];
486
+ const label = selected ? selected.item.label : "";
487
+ return `${prefix} ${msg} ${theme.success(label)}`;
488
+ }
489
+ async function filter(options) {
490
+ if (options.initial !== undefined) {
491
+ return options.initial;
492
+ }
493
+ const theme = resolveTheme(options.theme);
494
+ const maxVisible = options.maxVisible ?? DEFAULT_MAX_VISIBLE;
495
+ const choices = normalizeChoices(options.choices);
496
+ const initialResults = fuzzyFilter("", choices);
497
+ const initialState = {
498
+ query: "",
499
+ cursorPos: 0,
500
+ choices,
501
+ results: initialResults,
502
+ listCursor: 0,
503
+ scrollOffset: 0
504
+ };
505
+ return runPrompt({
506
+ initialState,
507
+ theme,
508
+ render: (state, t) => renderFilter(state, t, options.message, options.placeholder, maxVisible),
509
+ handleKey: createHandleKey(maxVisible),
510
+ renderSubmitted: (state, value, t) => renderSubmitted2(state, value, t, options.message, state.results, state.listCursor)
511
+ });
512
+ }
513
+ // src/prompts/input.ts
514
+ function createHandleKey2(validate, defaultValue) {
515
+ return async (key, state) => {
516
+ if (key.name === "return") {
517
+ const submitValue = state.value === "" && defaultValue !== undefined ? defaultValue : state.value;
518
+ if (validate) {
519
+ const result = await validate(submitValue);
520
+ if (result !== true) {
521
+ return { ...state, error: result };
522
+ }
523
+ }
524
+ return submit(submitValue);
525
+ }
526
+ const edit = handleTextEdit(key, state.value, state.cursorPos);
527
+ if (edit) {
528
+ return { value: edit.text, cursorPos: edit.cursorPos, error: null };
529
+ }
530
+ return state;
531
+ };
532
+ }
533
+ var PREFIX_SYMBOL3 = "?";
534
+ function renderInput(state, theme, message, placeholder, defaultValue) {
535
+ const prefix = theme.prefix(PREFIX_SYMBOL3);
536
+ const msg = theme.message(message);
537
+ const defaultHint = defaultValue !== undefined && state.value === "" ? ` ${theme.hint(`(${defaultValue})`)}` : "";
538
+ let valueLine;
539
+ if (state.value === "") {
540
+ if (placeholder) {
541
+ valueLine = theme.placeholder(placeholder);
542
+ } else {
543
+ valueLine = theme.cursor(CURSOR_CHAR);
544
+ }
545
+ } else {
546
+ const before = state.value.slice(0, state.cursorPos);
547
+ const after = state.value.slice(state.cursorPos);
548
+ valueLine = `${before}${theme.cursor(CURSOR_CHAR)}${after}`;
549
+ }
550
+ let output = `${prefix} ${msg}${defaultHint}
551
+ ${valueLine}`;
552
+ if (state.error !== null) {
553
+ output += `
554
+ ${theme.error(state.error)}`;
555
+ }
556
+ return output;
557
+ }
558
+ function renderSubmitted3(_state, value, theme, message) {
559
+ const prefix = theme.prefix(PREFIX_SYMBOL3);
560
+ const msg = theme.message(message);
561
+ return `${prefix} ${msg} ${theme.success(value)}`;
562
+ }
563
+ async function input(options) {
564
+ if (options.initial !== undefined) {
565
+ return options.initial;
566
+ }
567
+ const theme = resolveTheme(options.theme);
568
+ const initialState = {
569
+ value: "",
570
+ cursorPos: 0,
571
+ error: null
572
+ };
573
+ return runPrompt({
574
+ initialState,
575
+ theme,
576
+ render: (state, t) => renderInput(state, t, options.message, options.placeholder, options.default),
577
+ handleKey: createHandleKey2(options.validate, options.default),
578
+ renderSubmitted: (state, value, t) => renderSubmitted3(state, value, t, options.message)
579
+ });
580
+ }
581
+ // src/prompts/multiselect.ts
582
+ var DEFAULT_MAX_VISIBLE2 = 10;
583
+ var PREFIX_SYMBOL4 = "?";
584
+ var CURSOR_INDICATOR = ">";
585
+ var CHECKBOX_CHECKED = "[x]";
586
+ var CHECKBOX_UNCHECKED = "[ ]";
587
+ var SCROLL_UP_INDICATOR2 = "...";
588
+ var SCROLL_DOWN_INDICATOR2 = "...";
589
+ var HINT_LINE = "(Space to toggle, a to toggle all, i to invert, Enter to confirm)";
590
+ function validateSelection(selectedCount, required, min, max) {
591
+ if (required && selectedCount === 0) {
592
+ return "At least one item must be selected";
593
+ }
594
+ if (min !== undefined && selectedCount < min) {
595
+ return `Select at least ${min} item${min === 1 ? "" : "s"}`;
596
+ }
597
+ if (max !== undefined && selectedCount > max) {
598
+ return `Select at most ${max} item${max === 1 ? "" : "s"}`;
599
+ }
600
+ return null;
601
+ }
602
+ function createHandleKey3(maxVisible, required, min, max) {
603
+ return (key, state) => {
604
+ const totalItems = state.choices.length;
605
+ if (key.name === "return") {
606
+ const error = validateSelection(state.selected.size, required, min, max);
607
+ if (error) {
608
+ return { ...state, error };
609
+ }
610
+ const selectedValues = state.choices.filter((_, i) => state.selected.has(i)).map((c) => c.value);
611
+ return submit(selectedValues);
612
+ }
613
+ if (key.name === "space") {
614
+ const newSelected = new Set(state.selected);
615
+ if (newSelected.has(state.cursor)) {
616
+ newSelected.delete(state.cursor);
617
+ } else {
618
+ if (max === undefined || newSelected.size < max) {
619
+ newSelected.add(state.cursor);
620
+ }
621
+ }
622
+ return { ...state, selected: newSelected, error: null };
623
+ }
624
+ if (key.name === "a" && !key.ctrl && !key.meta) {
625
+ const allSelected = state.selected.size === totalItems;
626
+ let newSelected;
627
+ if (allSelected) {
628
+ newSelected = new Set;
629
+ } else {
630
+ newSelected = new Set;
631
+ for (let i = 0;i < totalItems; i++) {
632
+ if (max !== undefined && newSelected.size >= max)
633
+ break;
634
+ newSelected.add(i);
635
+ }
636
+ }
637
+ return { ...state, selected: newSelected, error: null };
638
+ }
639
+ if (key.name === "i" && !key.ctrl && !key.meta) {
640
+ const newSelected = new Set;
641
+ for (let i = 0;i < totalItems; i++) {
642
+ if (!state.selected.has(i)) {
643
+ if (max !== undefined && newSelected.size >= max)
644
+ break;
645
+ newSelected.add(i);
646
+ }
647
+ }
648
+ return { ...state, selected: newSelected, error: null };
649
+ }
650
+ if (key.name === "up" || key.name === "k") {
651
+ const newCursor = state.cursor <= 0 ? totalItems - 1 : state.cursor - 1;
652
+ const newScrollOffset = calculateScrollOffset(newCursor, state.scrollOffset, totalItems, maxVisible);
653
+ return {
654
+ ...state,
655
+ cursor: newCursor,
656
+ scrollOffset: newScrollOffset,
657
+ error: null
658
+ };
659
+ }
660
+ if (key.name === "down" || key.name === "j") {
661
+ const newCursor = state.cursor >= totalItems - 1 ? 0 : state.cursor + 1;
662
+ const newScrollOffset = calculateScrollOffset(newCursor, state.scrollOffset, totalItems, maxVisible);
663
+ return {
664
+ ...state,
665
+ cursor: newCursor,
666
+ scrollOffset: newScrollOffset,
667
+ error: null
668
+ };
669
+ }
670
+ return state;
671
+ };
672
+ }
673
+ function renderMultiselect(state, theme, message, maxVisible) {
674
+ const prefix = theme.prefix(PREFIX_SYMBOL4);
675
+ const msg = theme.message(message);
676
+ const totalItems = state.choices.length;
677
+ const visibleCount = Math.min(totalItems, maxVisible);
678
+ const lines = [`${prefix} ${msg}`];
679
+ lines.push(theme.hint(HINT_LINE));
680
+ const hasScrollUp = state.scrollOffset > 0;
681
+ if (hasScrollUp) {
682
+ lines.push(theme.hint(SCROLL_UP_INDICATOR2));
683
+ }
684
+ for (let i = 0;i < visibleCount; i++) {
685
+ const choiceIndex = state.scrollOffset + i;
686
+ const choice = state.choices[choiceIndex];
687
+ if (!choice)
688
+ break;
689
+ const isActive = choiceIndex === state.cursor;
690
+ const isChecked = state.selected.has(choiceIndex);
691
+ const checkbox = isChecked ? theme.success(CHECKBOX_CHECKED) : CHECKBOX_UNCHECKED;
692
+ const hintText = choice.hint ? ` ${theme.hint(choice.hint)}` : "";
693
+ if (isActive) {
694
+ lines.push(`${theme.cursor(CURSOR_INDICATOR)} ${checkbox} ${theme.selected(choice.label)}${hintText}`);
695
+ } else {
696
+ lines.push(` ${checkbox} ${theme.unselected(choice.label)}${hintText}`);
697
+ }
698
+ }
699
+ const hasScrollDown = state.scrollOffset + visibleCount < totalItems;
700
+ if (hasScrollDown) {
701
+ lines.push(theme.hint(SCROLL_DOWN_INDICATOR2));
702
+ }
703
+ if (state.error) {
704
+ lines.push(theme.error(state.error));
705
+ }
706
+ return lines.join(`
707
+ `);
708
+ }
709
+ function renderSubmitted4(_state, _value, theme, message, choices, selected) {
710
+ const prefix = theme.prefix(PREFIX_SYMBOL4);
711
+ const msg = theme.message(message);
712
+ const selectedLabels = choices.filter((_, i) => selected.has(i)).map((c) => c.label).join(", ");
713
+ return `${prefix} ${msg} ${theme.success(selectedLabels)}`;
714
+ }
715
+ async function multiselect(options) {
716
+ if (options.initial !== undefined) {
717
+ return [...options.initial];
718
+ }
719
+ const theme = resolveTheme(options.theme);
720
+ const maxVisible = options.maxVisible ?? DEFAULT_MAX_VISIBLE2;
721
+ const choices = normalizeChoices(options.choices);
722
+ const initialSelected = new Set;
723
+ if (options.default) {
724
+ for (const defaultValue of options.default) {
725
+ const idx = choices.findIndex((c) => c.value === defaultValue);
726
+ if (idx !== -1) {
727
+ initialSelected.add(idx);
728
+ }
729
+ }
730
+ }
731
+ const initialState = {
732
+ cursor: 0,
733
+ choices,
734
+ selected: initialSelected,
735
+ scrollOffset: 0,
736
+ error: null
737
+ };
738
+ return runPrompt({
739
+ initialState,
740
+ theme,
741
+ render: (state, t) => renderMultiselect(state, t, options.message, maxVisible),
742
+ handleKey: createHandleKey3(maxVisible, options.required, options.min, options.max),
743
+ renderSubmitted: (state, value, t) => renderSubmitted4(state, value, t, options.message, choices, state.selected)
744
+ });
745
+ }
746
+ // src/prompts/password.ts
747
+ function createHandleKey4(validate) {
748
+ return async (key, state) => {
749
+ if (key.name === "return") {
750
+ if (validate) {
751
+ const result = await validate(state.value);
752
+ if (result !== true) {
753
+ return { ...state, error: result };
754
+ }
755
+ }
756
+ return submit(state.value);
757
+ }
758
+ const edit = handleTextEdit(key, state.value, state.cursorPos);
759
+ if (edit) {
760
+ return { value: edit.text, cursorPos: edit.cursorPos, error: null };
761
+ }
762
+ return state;
763
+ };
764
+ }
765
+ var PREFIX_SYMBOL5 = "?";
766
+ var SUBMITTED_MASK_LENGTH = 4;
767
+ function renderPassword(state, theme, message, mask) {
768
+ const prefix = theme.prefix(PREFIX_SYMBOL5);
769
+ const msg = theme.message(message);
770
+ let valueLine;
771
+ if (state.value === "") {
772
+ valueLine = theme.cursor(CURSOR_CHAR);
773
+ } else {
774
+ const beforeMask = mask.repeat(state.cursorPos);
775
+ const afterMask = mask.repeat(state.value.length - state.cursorPos);
776
+ valueLine = `${beforeMask}${theme.cursor(CURSOR_CHAR)}${afterMask}`;
777
+ }
778
+ let output = `${prefix} ${msg}
779
+ ${valueLine}`;
780
+ if (state.error !== null) {
781
+ output += `
782
+ ${theme.error(state.error)}`;
783
+ }
784
+ return output;
785
+ }
786
+ function renderSubmitted5(_state, _value, theme, message, mask) {
787
+ const prefix = theme.prefix(PREFIX_SYMBOL5);
788
+ const msg = theme.message(message);
789
+ const maskedDisplay = theme.success(mask.repeat(SUBMITTED_MASK_LENGTH));
790
+ return `${prefix} ${msg} ${maskedDisplay}`;
791
+ }
792
+ async function password(options) {
793
+ if (options.initial !== undefined) {
794
+ return options.initial;
795
+ }
796
+ const theme = resolveTheme(options.theme);
797
+ const mask = options.mask ?? "*";
798
+ const initialState = {
799
+ value: "",
800
+ cursorPos: 0,
801
+ error: null
802
+ };
803
+ return runPrompt({
804
+ initialState,
805
+ theme,
806
+ render: (state, t) => renderPassword(state, t, options.message, mask),
807
+ handleKey: createHandleKey4(options.validate),
808
+ renderSubmitted: (state, value, t) => renderSubmitted5(state, value, t, options.message, mask)
809
+ });
810
+ }
811
+ // src/prompts/select.ts
812
+ var DEFAULT_MAX_VISIBLE3 = 10;
813
+ var PREFIX_SYMBOL6 = "?";
814
+ var CURSOR_INDICATOR2 = ">";
815
+ var SCROLL_UP_INDICATOR3 = "...";
816
+ var SCROLL_DOWN_INDICATOR3 = "...";
817
+ function createHandleKey5(maxVisible) {
818
+ return (key, state) => {
819
+ const totalItems = state.choices.length;
820
+ if (key.name === "return") {
821
+ const selected = state.choices[state.cursor];
822
+ if (selected) {
823
+ return submit(selected.value);
824
+ }
825
+ return state;
826
+ }
827
+ if (key.name === "up" || key.name === "k") {
828
+ const newCursor = state.cursor <= 0 ? totalItems - 1 : state.cursor - 1;
829
+ const newScrollOffset = calculateScrollOffset(newCursor, state.scrollOffset, totalItems, maxVisible);
830
+ return { ...state, cursor: newCursor, scrollOffset: newScrollOffset };
831
+ }
832
+ if (key.name === "down" || key.name === "j") {
833
+ const newCursor = state.cursor >= totalItems - 1 ? 0 : state.cursor + 1;
834
+ const newScrollOffset = calculateScrollOffset(newCursor, state.scrollOffset, totalItems, maxVisible);
835
+ return { ...state, cursor: newCursor, scrollOffset: newScrollOffset };
836
+ }
837
+ return state;
838
+ };
839
+ }
840
+ function renderSelect(state, theme, message, maxVisible) {
841
+ const prefix = theme.prefix(PREFIX_SYMBOL6);
842
+ const msg = theme.message(message);
843
+ const totalItems = state.choices.length;
844
+ const visibleCount = Math.min(totalItems, maxVisible);
845
+ const lines = [`${prefix} ${msg}`];
846
+ const hasScrollUp = state.scrollOffset > 0;
847
+ if (hasScrollUp) {
848
+ lines.push(theme.hint(SCROLL_UP_INDICATOR3));
849
+ }
850
+ for (let i = 0;i < visibleCount; i++) {
851
+ const choiceIndex = state.scrollOffset + i;
852
+ const choice = state.choices[choiceIndex];
853
+ if (!choice)
854
+ break;
855
+ const isActive = choiceIndex === state.cursor;
856
+ const hintText = choice.hint ? ` ${theme.hint(choice.hint)}` : "";
857
+ if (isActive) {
858
+ lines.push(`${theme.cursor(CURSOR_INDICATOR2)} ${theme.selected(choice.label)}${hintText}`);
859
+ } else {
860
+ lines.push(` ${theme.unselected(choice.label)}${hintText}`);
861
+ }
862
+ }
863
+ const hasScrollDown = state.scrollOffset + visibleCount < totalItems;
864
+ if (hasScrollDown) {
865
+ lines.push(theme.hint(SCROLL_DOWN_INDICATOR3));
866
+ }
867
+ return lines.join(`
868
+ `);
869
+ }
870
+ function renderSubmitted6(_state, _value, theme, message, choices, cursor) {
871
+ const prefix = theme.prefix(PREFIX_SYMBOL6);
872
+ const msg = theme.message(message);
873
+ const selected = choices[cursor];
874
+ const label = selected ? selected.label : "";
875
+ return `${prefix} ${msg} ${theme.success(label)}`;
876
+ }
877
+ async function select(options) {
878
+ if (options.initial !== undefined) {
879
+ return options.initial;
880
+ }
881
+ const theme = resolveTheme(options.theme);
882
+ const maxVisible = options.maxVisible ?? DEFAULT_MAX_VISIBLE3;
883
+ const choices = normalizeChoices(options.choices);
884
+ let initialCursor = 0;
885
+ if (options.default !== undefined) {
886
+ const idx = choices.findIndex((c) => c.value === options.default);
887
+ if (idx !== -1) {
888
+ initialCursor = idx;
889
+ }
890
+ }
891
+ const initialScrollOffset = calculateScrollOffset(initialCursor, 0, choices.length, maxVisible);
892
+ const initialState = {
893
+ cursor: initialCursor,
894
+ choices,
895
+ scrollOffset: initialScrollOffset
896
+ };
897
+ return runPrompt({
898
+ initialState,
899
+ theme,
900
+ render: (state, t) => renderSelect(state, t, options.message, maxVisible),
901
+ handleKey: createHandleKey5(maxVisible),
902
+ renderSubmitted: (state, value, t) => renderSubmitted6(state, value, t, options.message, choices, state.cursor)
903
+ });
904
+ }
905
+ // src/prompts/spinner.ts
906
+ var ESC2 = "\x1B[";
907
+ var HIDE_CURSOR2 = `${ESC2}?25l`;
908
+ var SHOW_CURSOR2 = `${ESC2}?25h`;
909
+ var ERASE_LINE2 = `${ESC2}2K`;
910
+ var CURSOR_TO_START = "\r";
911
+ var BUILTIN_SPINNERS = {
912
+ dots: {
913
+ frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
914
+ interval: 80
915
+ },
916
+ line: {
917
+ frames: ["-", "\\", "|", "/"],
918
+ interval: 130
919
+ },
920
+ arc: {
921
+ frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"],
922
+ interval: 100
923
+ },
924
+ bounce: {
925
+ frames: ["\u2801", "\u2802", "\u2804", "\u2840", "\u2880", "\u2820", "\u2810", "\u2808"],
926
+ interval: 120
927
+ }
928
+ };
929
+ function resolveSpinner(spinnerType) {
930
+ if (spinnerType === undefined) {
931
+ return BUILTIN_SPINNERS.dots;
932
+ }
933
+ if (typeof spinnerType === "string") {
934
+ return BUILTIN_SPINNERS[spinnerType];
935
+ }
936
+ return spinnerType;
937
+ }
938
+ var SUCCESS_SYMBOL = "\u2714";
939
+ var ERROR_SYMBOL = "\u2716";
940
+ function renderFrame(frame, message, theme) {
941
+ return `${ERASE_LINE2}${CURSOR_TO_START}${theme.spinner(frame)} ${theme.message(message)}`;
942
+ }
943
+ function renderSuccess(message, theme) {
944
+ return `${ERASE_LINE2}${CURSOR_TO_START}${theme.success(SUCCESS_SYMBOL)} ${theme.message(message)}
945
+ `;
946
+ }
947
+ function renderError(message, theme) {
948
+ return `${ERASE_LINE2}${CURSOR_TO_START}${theme.error(ERROR_SYMBOL)} ${theme.message(message)}
949
+ `;
950
+ }
951
+ async function spinner(options) {
952
+ const theme = resolveTheme(options.theme);
953
+ const { frames, interval } = resolveSpinner(options.spinner);
954
+ let frameIndex = 0;
955
+ let timerId;
956
+ process.stderr.write(HIDE_CURSOR2);
957
+ process.stderr.write(renderFrame(frames[0], options.message, theme));
958
+ timerId = setInterval(() => {
959
+ frameIndex = (frameIndex + 1) % frames.length;
960
+ process.stderr.write(renderFrame(frames[frameIndex], options.message, theme));
961
+ }, interval);
962
+ try {
963
+ const result = await options.task();
964
+ clearInterval(timerId);
965
+ timerId = undefined;
966
+ process.stderr.write(renderSuccess(options.message, theme));
967
+ process.stderr.write(SHOW_CURSOR2);
968
+ return result;
969
+ } catch (error) {
970
+ if (timerId !== undefined) {
971
+ clearInterval(timerId);
972
+ timerId = undefined;
973
+ }
974
+ process.stderr.write(renderError(options.message, theme));
975
+ process.stderr.write(SHOW_CURSOR2);
976
+ throw error;
977
+ }
978
+ }
979
+ export {
980
+ submit,
981
+ spinner,
982
+ setTheme,
983
+ select,
984
+ runPrompt,
985
+ password,
986
+ normalizeChoices,
987
+ multiselect,
988
+ input,
989
+ handleTextEdit,
990
+ getTheme,
991
+ fuzzyMatch,
992
+ fuzzyFilter,
993
+ filter,
994
+ defaultTheme,
995
+ createTheme,
996
+ confirm,
997
+ calculateScrollOffset,
998
+ assertTTY,
999
+ NonInteractiveError,
1000
+ CancelledError,
1001
+ CURSOR_CHAR
1002
+ };