@stacksjs/cli 0.65.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/app.ts DELETED
@@ -1,777 +0,0 @@
1
- /* eslint-disable ts/no-use-before-define, regexp/no-useless-non-capturing-group, regexp/no-trivially-nested-quantifier, regexp/no-useless-quantifier, regexp/prefer-w,regexp/no-useless-escape */
2
- import process from 'node:process'
3
- // slightly modified version of @clack/prompts
4
- // many thanks to bombshell-dev for the original work
5
- import {
6
- block,
7
- ConfirmPrompt,
8
- GroupMultiSelectPrompt,
9
- isCancel,
10
- MultiSelectPrompt,
11
- PasswordPrompt,
12
- SelectKeyPrompt,
13
- SelectPrompt,
14
- type State,
15
- TextPrompt,
16
- } from '@clack/core'
17
- import isUnicodeSupported from 'is-unicode-supported'
18
- import color from 'picocolors'
19
- import { cursor, erase } from 'sisteransi'
20
-
21
- export { isCancel } from '@clack/core'
22
-
23
- const unicode = isUnicodeSupported()
24
- const s = (c: string, fallback: string) => (unicode ? c : fallback)
25
- const S_STEP_ACTIVE = s('◆', '*')
26
- const S_STEP_CANCEL = s('■', 'x')
27
- const S_STEP_ERROR = s('▲', 'x')
28
- const S_STEP_SUBMIT = s('◇', 'o')
29
-
30
- const S_BAR_START = s('┌', 'T')
31
- const S_BAR = s('│', '|')
32
- const S_BAR_END = s('└', '—')
33
-
34
- const S_RADIO_ACTIVE = s('●', '>')
35
- const S_RADIO_INACTIVE = s('○', ' ')
36
- const S_CHECKBOX_ACTIVE = s('◻', '[•]')
37
- const S_CHECKBOX_SELECTED = s('◼', '[+]')
38
- const S_CHECKBOX_INACTIVE = s('◻', '[ ]')
39
- const S_PASSWORD_MASK = s('▪', '•')
40
-
41
- const S_BAR_H = s('─', '-')
42
- const S_CORNER_TOP_RIGHT = s('╮', '+')
43
- const S_CONNECT_LEFT = s('├', '+')
44
- const S_CORNER_BOTTOM_RIGHT = s('╯', '+')
45
-
46
- function symbol(state: State) {
47
- switch (state) {
48
- case 'initial':
49
- case 'active':
50
- return color.cyan(S_STEP_ACTIVE)
51
- case 'cancel':
52
- return color.red(S_STEP_CANCEL)
53
- case 'error':
54
- return color.yellow(S_STEP_ERROR)
55
- case 'submit':
56
- return color.green(S_STEP_SUBMIT)
57
- }
58
- }
59
-
60
- interface LimitOptionsParams<TOption> {
61
- options: TOption[]
62
- maxItems: number | undefined
63
- cursor: number
64
- style: (option: TOption, active: boolean) => string
65
- }
66
-
67
- function limitOptions<TOption>(params: LimitOptionsParams<TOption>): string[] {
68
- const { cursor, options, style } = params
69
-
70
- const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY
71
- const outputMaxItems = Math.max(process.stdout.rows - 4, 0)
72
- // We clamp to minimum 5 because anything less doesn't make sense UX wise
73
- const maxItems = Math.min(outputMaxItems, Math.max(paramMaxItems, 5))
74
- let slidingWindowLocation = 0
75
-
76
- if (cursor >= slidingWindowLocation + maxItems - 3) {
77
- slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0)
78
- }
79
- else if (cursor < slidingWindowLocation + 2) {
80
- slidingWindowLocation = Math.max(cursor - 2, 0)
81
- }
82
-
83
- const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0
84
- const shouldRenderBottomEllipsis = maxItems < options.length && slidingWindowLocation + maxItems < options.length
85
-
86
- return options.slice(slidingWindowLocation, slidingWindowLocation + maxItems).map((option, i, arr) => {
87
- const isTopLimit = i === 0 && shouldRenderTopEllipsis
88
- const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis
89
- return isTopLimit || isBottomLimit ? color.dim('...') : style(option, i + slidingWindowLocation === cursor)
90
- })
91
- }
92
-
93
- export interface TextOptions {
94
- message: string
95
- placeholder?: string
96
- defaultValue?: string
97
- initialValue?: string
98
- validate?: (value: string) => string | void
99
- }
100
- export function text(opts: TextOptions) {
101
- return new TextPrompt({
102
- validate: opts.validate,
103
- placeholder: opts.placeholder,
104
- defaultValue: opts.defaultValue,
105
- initialValue: opts.initialValue,
106
- render() {
107
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
108
- const placeholder = opts.placeholder
109
- ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1))
110
- : color.inverse(color.hidden('_'))
111
- const value = !this.value ? placeholder : this.valueWithCursor
112
-
113
- switch (this.state) {
114
- case 'error':
115
- return `${title.trim()}\n${color.yellow(S_BAR)} ${value}\n${color.yellow(
116
- S_BAR_END,
117
- )} ${color.yellow(this.error)}\n`
118
- case 'submit':
119
- return `${title}${color.gray(S_BAR)} ${color.dim(this.value || opts.placeholder)}`
120
- case 'cancel':
121
- return `${title}${color.gray(S_BAR)} ${color.strikethrough(
122
- color.dim(this.value ?? ''),
123
- )}${this.value?.trim() ? `\n${color.gray(S_BAR)}` : ''}`
124
- default:
125
- return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`
126
- }
127
- },
128
- }).prompt() as Promise<string | symbol>
129
- }
130
-
131
- export interface PasswordOptions {
132
- message: string
133
- mask?: string
134
- validate?: (value: string) => string | void
135
- }
136
- export function password(opts: PasswordOptions) {
137
- return new PasswordPrompt({
138
- validate: opts.validate,
139
- mask: opts.mask ?? S_PASSWORD_MASK,
140
- render() {
141
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
142
- const value = this.valueWithCursor
143
- const masked = this.masked
144
-
145
- switch (this.state) {
146
- case 'error':
147
- return `${title.trim()}\n${color.yellow(S_BAR)} ${masked}\n${color.yellow(
148
- S_BAR_END,
149
- )} ${color.yellow(this.error)}\n`
150
- case 'submit':
151
- return `${title}${color.gray(S_BAR)} ${color.dim(masked)}`
152
- case 'cancel':
153
- return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(masked ?? ''))}${
154
- masked ? `\n${color.gray(S_BAR)}` : ''
155
- }`
156
- default:
157
- return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`
158
- }
159
- },
160
- }).prompt() as Promise<string | symbol>
161
- }
162
-
163
- export interface ConfirmOptions {
164
- message: string
165
- active?: string
166
- inactive?: string
167
- initialValue?: boolean
168
- }
169
- export function confirm(opts: ConfirmOptions) {
170
- const active = opts.active ?? 'Yes'
171
- const inactive = opts.inactive ?? 'No'
172
- return new ConfirmPrompt({
173
- active,
174
- inactive,
175
- initialValue: opts.initialValue ?? true,
176
- render() {
177
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
178
- const value = this.value ? active : inactive
179
-
180
- switch (this.state) {
181
- case 'submit':
182
- return `${title}${color.gray(S_BAR)} ${color.dim(value)}`
183
- case 'cancel':
184
- return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(value))}\n${color.gray(S_BAR)}`
185
- default: {
186
- return `${title}${color.cyan(S_BAR)} ${
187
- this.value
188
- ? `${color.green(S_RADIO_ACTIVE)} ${active}`
189
- : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}`
190
- } ${color.dim('/')} ${
191
- !this.value
192
- ? `${color.green(S_RADIO_ACTIVE)} ${inactive}`
193
- : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}`
194
- }\n${color.cyan(S_BAR_END)}\n`
195
- }
196
- }
197
- },
198
- }).prompt() as Promise<boolean | symbol>
199
- }
200
-
201
- type Primitive = Readonly<string | boolean | number>
202
-
203
- type Option<Value> = Value extends Primitive
204
- ? { value: Value, label?: string, hint?: string }
205
- : { value: Value, label: string, hint?: string }
206
-
207
- export interface SelectOptions<Value> {
208
- message: string
209
- options: Option<Value>[]
210
- initialValue?: Value
211
- maxItems?: number
212
- }
213
-
214
- export function select<Value>(opts: SelectOptions<Value>) {
215
- const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled') => {
216
- const label = option.label ?? String(option.value)
217
- switch (state) {
218
- case 'selected':
219
- return `${color.dim(label)}`
220
- case 'active':
221
- return `${color.green(S_RADIO_ACTIVE)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`
222
- case 'cancelled':
223
- return `${color.strikethrough(color.dim(label))}`
224
- default:
225
- return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`
226
- }
227
- }
228
-
229
- return new SelectPrompt({
230
- options: opts.options,
231
- initialValue: opts.initialValue,
232
- render() {
233
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
234
-
235
- switch (this.state) {
236
- case 'submit':
237
- return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor]!, 'selected')}`
238
- case 'cancel':
239
- return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor]!, 'cancelled')}\n${color.gray(S_BAR)}`
240
- default: {
241
- return `${title}${color.cyan(S_BAR)} ${limitOptions({
242
- cursor: this.cursor,
243
- options: this.options,
244
- maxItems: opts.maxItems,
245
- style: (item, active) => opt(item, active ? 'active' : 'inactive'),
246
- }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`
247
- }
248
- }
249
- },
250
- }).prompt() as Promise<Value | symbol>
251
- }
252
-
253
- export function selectKey<Value extends string>(opts: SelectOptions<Value>) {
254
- const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive') => {
255
- const label = option.label ?? String(option.value)
256
-
257
- if (state === 'selected') {
258
- return `${color.dim(label)}`
259
- }
260
-
261
- if (state === 'cancelled') {
262
- return `${color.strikethrough(color.dim(label))}`
263
- }
264
-
265
- if (state === 'active') {
266
- return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${
267
- option.hint ? color.dim(`(${option.hint})`) : ''
268
- }`
269
- }
270
-
271
- return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${
272
- option.hint ? color.dim(`(${option.hint})`) : ''
273
- }`
274
- }
275
-
276
- return new SelectKeyPrompt({
277
- options: opts.options,
278
- initialValue: opts.initialValue,
279
- render() {
280
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
281
-
282
- switch (this.state) {
283
- case 'submit':
284
- return `${title}${color.gray(S_BAR)} ${opt(
285
- this.options.find(opt => opt.value === this.value)!,
286
- 'selected',
287
- )}`
288
- case 'cancel':
289
- return `${title}${color.gray(S_BAR)} ${opt(this.options[0]!, 'cancelled')}\n${color.gray(S_BAR)}`
290
- default: {
291
- return `${title}${color.cyan(S_BAR)} ${this.options
292
- .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive'))
293
- .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`
294
- }
295
- }
296
- },
297
- }).prompt() as Promise<Value | symbol>
298
- }
299
-
300
- export interface MultiSelectOptions<Value> {
301
- message: string
302
- options: Option<Value>[]
303
- initialValues?: Value[]
304
- maxItems?: number
305
- required?: boolean
306
- cursorAt?: Value
307
- }
308
- export function multiselect<Value>(opts: MultiSelectOptions<Value>) {
309
- const opt = (
310
- option: Option<Value>,
311
- state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled',
312
- ) => {
313
- const label = option.label ?? String(option.value)
314
-
315
- if (state === 'active') {
316
- return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`
317
- }
318
-
319
- if (state === 'selected') {
320
- return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`
321
- }
322
-
323
- if (state === 'cancelled') {
324
- return `${color.strikethrough(color.dim(label))}`
325
- }
326
-
327
- if (state === 'active-selected') {
328
- return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`
329
- }
330
-
331
- if (state === 'submitted') {
332
- return `${color.dim(label)}`
333
- }
334
-
335
- return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`
336
- }
337
-
338
- return new MultiSelectPrompt({
339
- options: opts.options,
340
- initialValues: opts.initialValues,
341
- required: opts.required ?? true,
342
- cursorAt: opts.cursorAt,
343
- validate(selected: Value[]) {
344
- if (this.required && selected.length === 0) {
345
- return `Please select at least one option.\n${color.reset(
346
- color.dim(
347
- `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray(
348
- color.bgWhite(color.inverse(' enter ')),
349
- )} to submit`,
350
- ),
351
- )}`
352
- }
353
- },
354
- render() {
355
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
356
-
357
- const styleOption = (option: Option<Value>, active: boolean) => {
358
- const selected = this.value.includes(option.value)
359
- if (active && selected) {
360
- return opt(option, 'active-selected')
361
- }
362
- if (selected) {
363
- return opt(option, 'selected')
364
- }
365
- return opt(option, active ? 'active' : 'inactive')
366
- }
367
-
368
- switch (this.state) {
369
- case 'submit': {
370
- return `${title}${color.gray(S_BAR)} ${
371
- this.options
372
- .filter(({ value }) => this.value.includes(value))
373
- .map(option => opt(option, 'submitted'))
374
- .join(color.dim(', ')) || color.dim('none')
375
- }`
376
- }
377
- case 'cancel': {
378
- const label = this.options
379
- .filter(({ value }) => this.value.includes(value))
380
- .map(option => opt(option, 'cancelled'))
381
- .join(color.dim(', '))
382
- return `${title}${color.gray(S_BAR)} ${label.trim() ? `${label}\n${color.gray(S_BAR)}` : ''}`
383
- }
384
- case 'error': {
385
- const footer = this.error
386
- .split('\n')
387
- .map((ln, i) => (i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`))
388
- .join('\n')
389
- return `
390
- ${title}
391
- ${color.yellow(S_BAR)}
392
- ${limitOptions({
393
- options: this.options,
394
- cursor: this.cursor,
395
- maxItems: opts.maxItems,
396
- style: styleOption,
397
- }).join(`\n${color.yellow(S_BAR)} `)}
398
- \n${footer}\n`
399
- }
400
- default: {
401
- return `${title}${color.cyan(S_BAR)} ${limitOptions({
402
- options: this.options,
403
- cursor: this.cursor,
404
- maxItems: opts.maxItems,
405
- style: styleOption,
406
- }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`
407
- }
408
- }
409
- },
410
- }).prompt() as Promise<Value[] | symbol>
411
- }
412
-
413
- export interface GroupMultiSelectOptions<Value> {
414
- message: string
415
- options: Record<string, Option<Value>[]>
416
- initialValues?: Value[]
417
- required?: boolean
418
- cursorAt?: Value
419
- }
420
- export function groupMultiselect<Value>(opts: GroupMultiSelectOptions<Value>) {
421
- const opt = (
422
- option: Option<Value>,
423
- state:
424
- | 'inactive'
425
- | 'active'
426
- | 'selected'
427
- | 'active-selected'
428
- | 'group-active'
429
- | 'group-active-selected'
430
- | 'submitted'
431
- | 'cancelled',
432
- options: Option<Value>[] = [],
433
- ) => {
434
- const label = option.label ?? String(option.value)
435
- const isItem = typeof (option as any).group === 'string'
436
- const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true })
437
- const isLast = isItem && (next as any).group === true
438
- const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ''
439
-
440
- if (state === 'active') {
441
- return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${
442
- option.hint ? color.dim(`(${option.hint})`) : ''
443
- }`
444
- }
445
-
446
- if (state === 'group-active') {
447
- return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`
448
- }
449
-
450
- if (state === 'group-active-selected') {
451
- return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`
452
- }
453
-
454
- if (state === 'selected') {
455
- return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`
456
- }
457
-
458
- if (state === 'cancelled') {
459
- return `${color.strikethrough(color.dim(label))}`
460
- }
461
-
462
- if (state === 'active-selected') {
463
- return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${
464
- option.hint ? color.dim(`(${option.hint})`) : ''
465
- }`
466
- }
467
-
468
- if (state === 'submitted') {
469
- return `${color.dim(label)}`
470
- }
471
-
472
- return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`
473
- }
474
-
475
- return new GroupMultiSelectPrompt({
476
- options: opts.options,
477
- initialValues: opts.initialValues,
478
- required: opts.required ?? true,
479
- cursorAt: opts.cursorAt,
480
- validate(selected: Value[]) {
481
- if (this.required && selected.length === 0) {
482
- return `Please select at least one option.\n${color.reset(
483
- color.dim(
484
- `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray(
485
- color.bgWhite(color.inverse(' enter ')),
486
- )} to submit`,
487
- ),
488
- )}`
489
- }
490
- },
491
- render() {
492
- const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`
493
-
494
- switch (this.state) {
495
- case 'submit': {
496
- return `${title}${color.gray(S_BAR)} ${this.options
497
- .filter(({ value }) => this.value.includes(value))
498
- .map(option => opt(option, 'submitted'))
499
- .join(color.dim(', '))}`
500
- }
501
- case 'cancel': {
502
- const label = this.options
503
- .filter(({ value }) => this.value.includes(value))
504
- .map(option => opt(option, 'cancelled'))
505
- .join(color.dim(', '))
506
- return `${title}${color.gray(S_BAR)} ${label.trim() ? `${label}\n${color.gray(S_BAR)}` : ''}`
507
- }
508
- case 'error': {
509
- const footer = this.error
510
- .split('\n')
511
- .map((ln, i) => (i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`))
512
- .join('\n')
513
- return `${title}${color.yellow(S_BAR)} ${this.options
514
- .map((option, i, options) => {
515
- const selected
516
- = this.value.includes(option.value) || (option.group === true && this.isGroupSelected(`${option.value}`))
517
- const active = i === this.cursor
518
- const groupActive
519
- = !active && typeof option.group === 'string' && this.options[this.cursor]!.value === option.group
520
- if (groupActive) {
521
- return opt(option, selected ? 'group-active-selected' : 'group-active', options)
522
- }
523
- if (active && selected) {
524
- return opt(option, 'active-selected', options)
525
- }
526
- if (selected) {
527
- return opt(option, 'selected', options)
528
- }
529
- return opt(option, active ? 'active' : 'inactive', options)
530
- })
531
- .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`
532
- }
533
- default: {
534
- return `${title}${color.cyan(S_BAR)} ${this.options
535
- .map((option, i, options) => {
536
- const selected
537
- = this.value.includes(option.value) || (option.group === true && this.isGroupSelected(`${option.value}`))
538
- const active = i === this.cursor
539
- const groupActive
540
- = !active && typeof option.group === 'string' && this.options[this.cursor]!.value === option.group
541
- if (groupActive) {
542
- return opt(option, selected ? 'group-active-selected' : 'group-active', options)
543
- }
544
-
545
- if (active && selected) {
546
- return opt(option, 'active-selected', options)
547
- }
548
-
549
- if (selected) {
550
- return opt(option, 'selected', options)
551
- }
552
-
553
- return opt(option, active ? 'active' : 'inactive', options)
554
- })
555
- .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`
556
- }
557
- }
558
- },
559
- }).prompt() as Promise<Value[] | symbol>
560
- }
561
-
562
- const strip = (str: string) => str.replace(ansiRegex(), '')
563
- export function note(message = '', title = ''): void {
564
- const lines = `\n${message}\n`.split('\n')
565
- const titleLen = strip(title).length
566
- const len
567
- = Math.max(
568
- lines.reduce((sum, ln) => {
569
- ln = strip(ln)
570
- return ln.length > sum ? ln.length : sum
571
- }, 0),
572
- titleLen,
573
- ) + 2
574
- const msg = lines
575
- .map(ln => `${color.gray(S_BAR)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray(S_BAR)}`)
576
- .join('\n')
577
- process.stdout.write(
578
- `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray(
579
- S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT,
580
- )}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n`,
581
- )
582
- }
583
-
584
- export function cancel(message = ''): void {
585
- process.stdout.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`)
586
- }
587
-
588
- export function intro(title = ''): void {
589
- process.stdout.write(`${color.gray(S_BAR_START)} ${title}\n`)
590
- }
591
-
592
- export function outro(message = ''): void {
593
- process.stdout.write(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n`)
594
- }
595
-
596
- interface Spinner {
597
- start: (msg: string) => void
598
- stop: (msg: string, code: number) => void
599
- message: (msg: string) => void
600
- }
601
-
602
- export function spinner(): Spinner {
603
- const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']
604
- const delay = unicode ? 80 : 120
605
-
606
- let unblock: () => void
607
- let loop: NodeJS.Timeout
608
- let isSpinnerActive = false
609
- let _message = ''
610
-
611
- const handleExit = (code: number) => {
612
- const msg = code > 1 ? 'Something went wrong' : 'Canceled'
613
- if (isSpinnerActive)
614
- stop(msg, code)
615
- }
616
-
617
- const errorEventHandler = () => handleExit(2)
618
- const signalEventHandler = () => handleExit(1)
619
-
620
- const registerHooks = () => {
621
- // Reference: https://nodejs.org/api/process.html#event-uncaughtexception
622
- process.on('uncaughtExceptionMonitor', errorEventHandler)
623
- // Reference: https://nodejs.org/api/process.html#event-unhandledrejection
624
- process.on('unhandledRejection', errorEventHandler)
625
- // Reference Signal Events: https://nodejs.org/api/process.html#signal-events
626
- process.on('SIGINT', signalEventHandler)
627
- process.on('SIGTERM', signalEventHandler)
628
- process.on('exit', handleExit)
629
- }
630
-
631
- const clearHooks = () => {
632
- process.removeListener('uncaughtExceptionMonitor', errorEventHandler)
633
- process.removeListener('unhandledRejection', errorEventHandler)
634
- process.removeListener('SIGINT', signalEventHandler)
635
- process.removeListener('SIGTERM', signalEventHandler)
636
- process.removeListener('exit', handleExit)
637
- }
638
-
639
- const start = (msg = ''): void => {
640
- isSpinnerActive = true
641
- unblock = block()
642
- _message = msg.replace(/\.+$/, '')
643
- process.stdout.write(`${color.gray(S_BAR)}\n`)
644
- let frameIndex = 0
645
- let dotsTimer = 0
646
-
647
- registerHooks()
648
-
649
- setInterval(() => {
650
- const frame = color.magenta(frames[frameIndex])
651
- const loadingDots = '.'.repeat(Math.floor(dotsTimer)).slice(0, 3)
652
- process.stdout.write(cursor.move(-999, 0))
653
- process.stdout.write(erase.down(1))
654
- process.stdout.write(`${frame} ${_message}${loadingDots}`)
655
- frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0
656
- dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0
657
- }, delay)
658
- }
659
-
660
- const stop = (msg = '', code = 0): void => {
661
- _message = msg ?? _message
662
- isSpinnerActive = false
663
- clearInterval(loop as NodeJS.Timeout)
664
- const step
665
- = code === 0 ? color.green(S_STEP_SUBMIT) : code === 1 ? color.red(S_STEP_CANCEL) : color.red(S_STEP_ERROR)
666
- process.stdout.write(cursor.move(-999, 0))
667
- process.stdout.write(erase.down(1))
668
- process.stdout.write(`${step} ${_message}\n`)
669
- clearHooks()
670
- unblock()
671
- }
672
-
673
- const message = (msg = ''): void => {
674
- _message = msg ?? _message
675
- }
676
-
677
- const spinner = {
678
- start,
679
- stop,
680
- message,
681
- }
682
-
683
- return spinner
684
- }
685
-
686
- // Adapted from https://github.com/chalk/ansi-regex
687
- // @see LICENSE
688
- function ansiRegex() {
689
- const pattern = [
690
- '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
691
- '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
692
- ].join('|')
693
-
694
- return new RegExp(pattern, 'g')
695
- }
696
-
697
- export type PromptGroupAwaitedReturn<T> = {
698
- [P in keyof T]: Exclude<Awaited<T[P]>, symbol>
699
- }
700
-
701
- export interface PromptGroupOptions<T> {
702
- /**
703
- * Control how the group can be canceled
704
- * if one of the prompts is canceled.
705
- */
706
- onCancel?: (opts: { results: Prettify<Partial<PromptGroupAwaitedReturn<T>>> }) => void
707
- }
708
-
709
- type Prettify<T> = {
710
- [P in keyof T]: T[P]
711
- } & {}
712
-
713
- export type PromptGroup<T> = {
714
- [P in keyof T]: (opts: {
715
- results: Prettify<Partial<PromptGroupAwaitedReturn<Omit<T, P>>>>
716
- }) => void | Promise<T[P] | void>
717
- }
718
-
719
- /**
720
- * Define a group of prompts to be displayed
721
- * and return a results of objects within the group
722
- */
723
- export async function group<T>(prompts: PromptGroup<T>, opts?: PromptGroupOptions<T>): Promise<Prettify<PromptGroupAwaitedReturn<T>>> {
724
- const results = {} as any
725
- const promptNames = Object.keys(prompts)
726
-
727
- for (const name of promptNames) {
728
- const prompt = prompts[name as keyof T]
729
- const result = await prompt({ results })?.catch((e) => {
730
- throw e
731
- })
732
-
733
- // Pass the results to the onCancel function
734
- // so the user can decide what to do with the results
735
- // TODO: Switch to callback within core to avoid isCancel Fn
736
- if (typeof opts?.onCancel === 'function' && isCancel(result)) {
737
- results[name] = 'canceled'
738
- opts.onCancel({ results })
739
- continue
740
- }
741
-
742
- results[name] = result
743
- }
744
-
745
- return results
746
- }
747
-
748
- export interface Task {
749
- /**
750
- * Task title
751
- */
752
- title: string
753
- /**
754
- * Task function
755
- */
756
- task: (message: (string: string) => void) => string | Promise<string> | void | Promise<void>
757
-
758
- /**
759
- * If enabled === false the task will be skipped
760
- */
761
- enabled?: boolean
762
- }
763
-
764
- /**
765
- * Define a group of tasks to be executed
766
- */
767
- export async function tasks(tasks: Task[]): Promise<void> {
768
- for (const task of tasks) {
769
- if (task.enabled === false)
770
- continue
771
-
772
- const s = spinner()
773
- s.start(task.title)
774
- const result = await task.task(s.message)
775
- s.stop(result || task.title, 0)
776
- }
777
- }