@elysiumjs/core 0.4.2 → 0.5.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/command.ts DELETED
@@ -1,908 +0,0 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { Class } from 'type-fest';
16
-
17
- import { isArray, trim } from 'radash';
18
-
19
- import { ConsoleFormat, InteractsWithConsole } from './console';
20
- import { Symbols } from './utils';
21
-
22
- /**
23
- * Supported command argument types.
24
- * @author Axel Nana <axel.nana@workbud.com>
25
- */
26
- export enum CommandArgumentType {
27
- /**
28
- * String argument.
29
- */
30
- STRING = 'string',
31
-
32
- /**
33
- * Number argument.
34
- */
35
- NUMBER = 'number',
36
-
37
- /**
38
- * Boolean argument.
39
- */
40
- BOOLEAN = 'boolean',
41
-
42
- /**
43
- * Enum argument.
44
- */
45
- ENUM = 'enum',
46
-
47
- /**
48
- * Array argument.
49
- */
50
- ARRAY = 'array'
51
- }
52
-
53
- /**
54
- * Properties required when declaring a command argument.
55
- * @author Axel Nana <axel.nana@workbud.com>
56
- *
57
- * These properties are used to define command arguments using the `@arg()` decorator. The
58
- * values of these properties are used to generate the help message for the command.
59
- */
60
- export type CommandArgumentProps = {
61
- /**
62
- * The name of the argument.
63
- */
64
- name: string;
65
-
66
- /**
67
- * The description of the argument.
68
- */
69
- description?: string;
70
-
71
- /**
72
- * Whether the argument is required.
73
- */
74
- required?: boolean;
75
-
76
- /**
77
- * The default value for the argument.
78
- */
79
- default?: any;
80
-
81
- /**
82
- * The type of the argument.
83
- */
84
- type?: CommandArgumentType;
85
-
86
- /**
87
- * The list of allowed values for the argument.
88
- */
89
- enum?: string[];
90
-
91
- /**
92
- * The type of elements in the array.
93
- */
94
- arrayType?: CommandArgumentType; // Type of elements in the array
95
- };
96
-
97
- /**
98
- * Metadata for storing command arguments.
99
- * @author Axel Nana <axel.nana@workbud.com>
100
- */
101
- type CommandArgumentMetadata = CommandArgumentProps & {
102
- /**
103
- * The name of the class property that the argument is associated with.
104
- */
105
- propertyKey: string;
106
- };
107
-
108
- /**
109
- * Type for a command class.
110
- * @author Axel Nana <axel.nana@workbud.com>
111
- */
112
- export type CommandClass = Class<Command> & {
113
- /**
114
- * The name of the command.
115
- */
116
- readonly command: string;
117
-
118
- /**
119
- * The description of the command.
120
- */
121
- readonly description: string;
122
- };
123
-
124
- /**
125
- * Base class for commands.
126
- *
127
- * Commands are classes that can be executed through the CLI. They can have arguments
128
- * that can be passed to them using the `--` syntax.
129
- *
130
- * @author Axel Nana <axel.nana@workbud.com>
131
- */
132
- export abstract class Command extends InteractsWithConsole {
133
- //#region Command Interface
134
-
135
- /**
136
- * The name of the command.
137
- *
138
- * This is used to identify the command in the CLI when using the `exec` command.
139
- */
140
- public static readonly command: string = 'command';
141
-
142
- /**
143
- * The description of the command.
144
- *
145
- * This is displayed when displaying help messages for the command.
146
- */
147
- public static readonly description: string = 'Command description';
148
-
149
- /**
150
- * Decorator for registering a command argument.
151
- * @author Axel Nana <axel.nana@workbud.com>
152
- * @param name The name of the argument.
153
- * @param props Additional options for the argument.
154
- */
155
- public static arg(props: Partial<CommandArgumentProps> = {}): PropertyDecorator {
156
- return function (target, propertyKey) {
157
- const args: CommandArgumentMetadata[] =
158
- Reflect.getMetadata(Symbols.arg, target.constructor) ?? [];
159
-
160
- const propertyType = Reflect.getMetadata('design:type', target, propertyKey);
161
-
162
- // Infer argument type from property type if not explicitly specified
163
- let inferredType = props.type;
164
-
165
- if (!inferredType && propertyType) {
166
- if (propertyType === Array) {
167
- inferredType = CommandArgumentType.ARRAY;
168
-
169
- if (!props.arrayType) {
170
- props = {
171
- ...props,
172
- arrayType: CommandArgumentType.STRING
173
- };
174
- }
175
- } else if (propertyType.constructor !== Object) {
176
- switch (propertyType.name.toLowerCase()) {
177
- case 'number':
178
- inferredType = CommandArgumentType.NUMBER;
179
- break;
180
- case 'boolean':
181
- inferredType = CommandArgumentType.BOOLEAN;
182
- break;
183
- case 'string':
184
- inferredType = CommandArgumentType.STRING;
185
- break;
186
- default:
187
- inferredType = undefined;
188
- }
189
- }
190
- // Check if it's an enum by examining if the property type has values
191
- else if (Object.values(propertyType).length > 0) {
192
- inferredType = CommandArgumentType.ENUM;
193
-
194
- if (!props.enum) {
195
- props = {
196
- ...props,
197
- enum: Object.values(propertyType)
198
- };
199
- }
200
- }
201
- }
202
-
203
- args.push({
204
- name: props.name ?? (propertyKey as string),
205
- description: props.description,
206
- required: props.required ?? false,
207
- default: props.default,
208
- type: inferredType,
209
- enum: props.enum,
210
- arrayType: props.arrayType,
211
- propertyKey: propertyKey as string
212
- });
213
-
214
- Reflect.defineMetadata(Symbols.arg, args, target.constructor);
215
- };
216
- }
217
-
218
- /**
219
- * The command entry point. This method is called when the command is executed using the CLI.
220
- */
221
- public abstract run(): Promise<void>;
222
-
223
- /**
224
- * Initialize the command before it is executed. This method is called before the `run()` method.
225
- * and is used to validate the arguments and initialize the command.
226
- * @param argv The command line arguments.
227
- * @returns Whether the command was initialized successfully.
228
- */
229
- public async init(...argv: string[]): Promise<boolean> {
230
- const args = this.processArgs(argv);
231
- const validation = this.validateArgs(args);
232
-
233
- if (!validation.valid) {
234
- if (validation.missing.length > 0) {
235
- console.error(`Missing required arguments: ${validation.missing.join(', ')}\n`);
236
- }
237
-
238
- return false;
239
- }
240
-
241
- // Return false if help argument is provided
242
- if (Object.keys(args).includes('help')) {
243
- return false;
244
- }
245
-
246
- // Map CLI arguments to class properties using propertyKey from metadata
247
- const commandArgs = this.getArguments();
248
- for (const arg of commandArgs) {
249
- if (args[arg.name] !== undefined) {
250
- // Use the propertyKey to set the correct class property
251
- (this as any)[arg.propertyKey] = args[arg.name];
252
- }
253
- }
254
-
255
- return true;
256
- }
257
-
258
- /**
259
- * Get all registered arguments for this command.
260
- * @returns The list of registered arguments.
261
- */
262
- protected getArguments(): CommandArgumentMetadata[] {
263
- return Reflect.getMetadata(Symbols.arg, this.constructor) || [];
264
- }
265
-
266
- /**
267
- * Generate help text for the command.
268
- * @returns Formatted help text.
269
- */
270
- public async help(): Promise<string> {
271
- let helpText = `${this.bold('Command:')} ${this.format((this.constructor as CommandClass).command, ConsoleFormat.MAGENTA)}\n`;
272
- helpText += `${this.bold('Description:')} ${(this.constructor as CommandClass).description}\n\n`;
273
-
274
- const args = this.getArguments();
275
-
276
- if (args.length > 0) {
277
- helpText += `${this.bold('Arguments:')}\n`;
278
-
279
- for (const arg of args) {
280
- const requiredText = this.format(
281
- arg.required ? '(required)' : '(optional)',
282
- ConsoleFormat.RED,
283
- ConsoleFormat.BOLD
284
- );
285
- const defaultText =
286
- arg.default !== undefined
287
- ? this.format(`[default: ${arg.default}]`, ConsoleFormat.GRAY, ConsoleFormat.ITALIC)
288
- : '';
289
-
290
- // Generate type text
291
- let typeText = '';
292
- if (arg.type === CommandArgumentType.ARRAY) {
293
- typeText = `<array of ${arg.arrayType || 'string'}s>`;
294
- } else if (arg.type) {
295
- typeText = `<${arg.type}>`;
296
- }
297
-
298
- // Add enum values to help text if applicable
299
- const enumText = arg.enum ? ` [${arg.enum.join('|')}]` : '';
300
-
301
- helpText += ` ${this.format(`--${arg.name}`, ConsoleFormat.CYAN)} ${typeText}${enumText} ${requiredText} ${defaultText}\n`;
302
-
303
- if (arg.description) {
304
- helpText += ` ${arg.description}\n\n`;
305
- }
306
- }
307
- } else {
308
- helpText += 'This command has no arguments.\n';
309
- }
310
-
311
- return trim(helpText, '\n');
312
- }
313
-
314
- /**
315
- * Validate provided arguments against registered arguments.
316
- * @param args Arguments to validate.
317
- * @returns Object with validation result and missing required args if any.
318
- */
319
- protected validateArgs(args: Record<string, any>): { valid: boolean; missing: string[] } {
320
- const missing: string[] = [];
321
- const invalid: string[] = [];
322
- const commandArgs = this.getArguments();
323
-
324
- // Skip validation if help argument is provided
325
- if (Object.keys(args).includes('help')) {
326
- return { valid: true, missing: [] };
327
- }
328
-
329
- for (const arg of commandArgs) {
330
- // Check for missing required arguments
331
- if (arg.required && (args[arg.name] === undefined || args[arg.name] === null)) {
332
- missing.push(arg.name);
333
- continue;
334
- }
335
-
336
- // Skip validation if argument is not provided
337
- if (args[arg.name] === undefined) {
338
- continue;
339
- }
340
-
341
- // Validate enum values
342
- if (arg.type === CommandArgumentType.ENUM && arg.enum && !arg.enum.includes(args[arg.name])) {
343
- invalid.push(`${arg.name} (must be one of: ${arg.enum.join(', ')})`);
344
- }
345
-
346
- // Validate array of enum values
347
- if (
348
- arg.type === CommandArgumentType.ARRAY &&
349
- arg.arrayType === CommandArgumentType.ENUM &&
350
- arg.enum
351
- ) {
352
- const invalidItems = (args[arg.name] as any[]).filter((item) => !arg.enum!.includes(item));
353
- if (invalidItems.length > 0) {
354
- invalid.push(
355
- `${arg.name} (contains invalid values: ${invalidItems.join(', ')}. Must be one of: ${arg.enum.join(', ')})`
356
- );
357
- }
358
- }
359
- }
360
-
361
- if (invalid.length > 0) {
362
- console.error(`Invalid argument values: ${invalid.join(', ')}\n`);
363
- }
364
-
365
- return {
366
- valid: missing.length === 0 && invalid.length === 0,
367
- missing
368
- };
369
- }
370
-
371
- /**
372
- * Process command line arguments and apply default values.
373
- * @param argv The command line arguments.
374
- * @returns Processed arguments with defaults applied.
375
- */
376
- protected processArgs(argv: string[]): Record<string, any> {
377
- const processed: Record<string, any> = {};
378
- const commandArgs = this.getArguments();
379
-
380
- // Process command line arguments
381
- for (let i = 0; i < argv.length; i++) {
382
- const arg = argv[i];
383
-
384
- // Check if it's an argument (starts with --)
385
- if (arg.startsWith('--')) {
386
- const argName = arg.slice(2); // Remove -- prefix
387
-
388
- // Check if the next item is a value (not another argument)
389
- if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
390
- processed[argName] = argv[i + 1];
391
- i++; // Skip the value in the next iteration
392
- } else {
393
- // Flag argument (boolean)
394
- processed[argName] = true;
395
- }
396
- }
397
- }
398
-
399
- // Apply default values and type conversions
400
- for (const arg of commandArgs) {
401
- // Apply default values for missing arguments
402
- if (processed[arg.name] === undefined && arg.default !== undefined) {
403
- processed[arg.name] = arg.default;
404
- continue;
405
- }
406
-
407
- // Skip if no value to convert
408
- if (processed[arg.name] === undefined) {
409
- continue;
410
- }
411
-
412
- // Convert values based on type
413
- if (typeof processed[arg.name] === 'string') {
414
- // Determine the type to convert to
415
- const type = arg.type || this.inferType(arg.default);
416
-
417
- switch (type) {
418
- case CommandArgumentType.NUMBER:
419
- processed[arg.name] = Number(processed[arg.name]);
420
- break;
421
-
422
- case CommandArgumentType.BOOLEAN:
423
- const value = processed[arg.name].toLowerCase();
424
- processed[arg.name] = value === 'true' || value === '1' || value === 'yes';
425
- break;
426
-
427
- case CommandArgumentType.ENUM:
428
- // For enum types, we keep the string value but validate it in validateArgs
429
- // If it's a number enum, convert to number
430
- if (arg.enum && arg.enum.length > 0 && typeof arg.enum[0] === 'number') {
431
- processed[arg.name] = Number(processed[arg.name]);
432
- }
433
- break;
434
-
435
- case CommandArgumentType.ARRAY:
436
- // Split comma-separated values into an array
437
- const arrayValues = processed[arg.name].split(',').map((item: string) => item.trim());
438
-
439
- // Convert array elements based on arrayType
440
- if (arg.arrayType) {
441
- processed[arg.name] = arrayValues.map((item: string) => {
442
- switch (arg.arrayType) {
443
- case CommandArgumentType.NUMBER:
444
- return Number(item);
445
- case CommandArgumentType.BOOLEAN:
446
- return (
447
- item.toLowerCase() === 'true' ||
448
- item.toLowerCase() === '1' ||
449
- item.toLowerCase() === 'yes'
450
- );
451
- case CommandArgumentType.ENUM:
452
- // For enum arrays, validate each value in validateArgs
453
- return item;
454
- default:
455
- return item; // Default to string
456
- }
457
- });
458
- } else {
459
- processed[arg.name] = arrayValues; // Default to string array
460
- }
461
- break;
462
-
463
- case CommandArgumentType.STRING:
464
- default:
465
- // Keep as string
466
- break;
467
- }
468
- }
469
- }
470
-
471
- return processed;
472
- }
473
-
474
- //#endregion
475
-
476
- //#region UI
477
-
478
- /**
479
- * Start a progress bar in the console.
480
- * @param total The total number of items to process.
481
- * @param title Optional title for the progress bar.
482
- */
483
- protected progress(total: number, title: string = 'Processing'): CommandProgressBar {
484
- return new CommandProgressBar(total, title);
485
- }
486
-
487
- /**
488
- * Start a spinner in the console.
489
- * @param title Optional title for the spinner.
490
- * @param frames Optional custom frames for the spinner animation.
491
- * @param frameDelay Optional delay between frames in milliseconds.
492
- */
493
- protected spinner(
494
- title: string = 'Processing',
495
- frames?: string[],
496
- frameDelay?: number
497
- ): CommandSpinner {
498
- return new CommandSpinner(title, frames, frameDelay);
499
- }
500
-
501
- //#endregion
502
-
503
- //#region Utilities
504
-
505
- /**
506
- * Infer the type of an argument based on its default value.
507
- * @param defaultValue The default value.
508
- * @returns The inferred argument type.
509
- */
510
- private inferType(defaultValue: any): CommandArgumentType {
511
- if (defaultValue === undefined) {
512
- return CommandArgumentType.STRING;
513
- }
514
-
515
- const type = typeof defaultValue;
516
-
517
- switch (type) {
518
- case 'number':
519
- return CommandArgumentType.NUMBER;
520
- case 'boolean':
521
- return CommandArgumentType.BOOLEAN;
522
- default:
523
- return isArray(defaultValue) ? CommandArgumentType.ARRAY : CommandArgumentType.STRING;
524
- }
525
- }
526
-
527
- //#endregion
528
- }
529
-
530
- /**
531
- * A class for managing and displaying a progress bar in the console.
532
- * @author Axel Nana <axel.nana@workbud.com>
533
- */
534
- class CommandProgressBar extends InteractsWithConsole {
535
- /**
536
- * Whether the progress bar is currently active.
537
- */
538
- #active: boolean = false;
539
-
540
- /**
541
- * The total number of steps to process.
542
- */
543
- #total: number = 0;
544
-
545
- /**
546
- * The current progress value.
547
- * This is incremented by the `update()` method.
548
- */
549
- #current: number = 0;
550
-
551
- /**
552
- * The title of the progress bar.
553
- */
554
- #title: string = '';
555
-
556
- /**
557
- * The start time of the progress bar.
558
- */
559
- #startTime: number = 0;
560
-
561
- /**
562
- * Create a new progress bar instance.
563
- * @param total The total number of items to process.
564
- * @param title Optional title for the progress bar.
565
- */
566
- constructor(total: number = 0, title: string = 'Processing') {
567
- super();
568
-
569
- this.#total = total;
570
- this.#title = title;
571
-
572
- this.#active = true;
573
- this.#current = 0;
574
- this.#startTime = Date.now();
575
-
576
- // Draw initial progress bar
577
- this.update(0);
578
- }
579
-
580
- /**
581
- * Update the progress bar with a new value.
582
- * @param increment The number of items to increment by (default: 1).
583
- * @param newTitle Optional new title for the progress bar.
584
- */
585
- public update(increment: number = 1, newTitle?: string): void {
586
- if (!this.#active) return;
587
-
588
- // Update progress
589
- this.#current += increment;
590
- if (newTitle) this.#title = newTitle;
591
-
592
- // Calculate percentage
593
- const percentage = Math.min(100, Math.floor((this.#current / this.#total) * 100));
594
-
595
- // Calculate elapsed time and estimated time remaining
596
- const elapsedMs = Date.now() - this.#startTime;
597
- const elapsedSec = elapsedMs / 1000;
598
-
599
- let estimatedTotalSec = 0;
600
- let estimatedRemainingSec = 0;
601
- let timeInfo = '';
602
-
603
- if (this.#current > 0) {
604
- estimatedTotalSec = (elapsedSec / this.#current) * this.#total;
605
- estimatedRemainingSec = Math.max(0, estimatedTotalSec - elapsedSec);
606
-
607
- timeInfo = ` | ${this.formatTime(elapsedSec)} elapsed | ${this.formatTime(estimatedRemainingSec)} remaining`;
608
- }
609
-
610
- // Create the progress bar
611
- const barWidth = 30;
612
- const completedWidth = Math.floor((percentage / 100) * barWidth);
613
- const remainingWidth = barWidth - completedWidth;
614
-
615
- const bar =
616
- '[' +
617
- '='.repeat(completedWidth) +
618
- (remainingWidth > 0 ? '>' : '') +
619
- ' '.repeat(Math.max(0, remainingWidth - (remainingWidth > 0 ? 1 : 0))) +
620
- ']';
621
-
622
- // Create the full progress line
623
- const progressLine = `${this.#title}: ${bar} ${percentage}% (${this.#current}/${this.#total})${timeInfo}`;
624
-
625
- // Clear the current line and write the new progress
626
- this.clearLine();
627
- this.write(progressLine, false);
628
-
629
- // If we're done, add a newline
630
- if (this.#current >= this.#total) {
631
- this.complete();
632
- }
633
- }
634
-
635
- /**
636
- * Complete the progress bar and reset its state.
637
- * @param message Optional completion message to display.
638
- */
639
- public complete(message?: string): void {
640
- if (!this.#active) return;
641
-
642
- // Ensure we show 100% completion
643
- this.#current = this.#total;
644
-
645
- // Calculate final elapsed time
646
- const elapsedMs = Date.now() - this.#startTime;
647
- const elapsedSec = elapsedMs / 1000;
648
-
649
- // Clear the current line
650
- this.clearLine();
651
-
652
- // Write completion message
653
- if (message) {
654
- this.write(`${message} (completed in ${this.formatTime(elapsedSec)})`);
655
- } else {
656
- this.write(`${this.#title} completed in ${this.formatTime(elapsedSec)}`);
657
- }
658
-
659
- // Reset progress bar state
660
- this.#active = false;
661
- }
662
-
663
- /**
664
- * Pause the progress bar temporarily to display a message.
665
- * @param message The message to display.
666
- */
667
- public pause(message: string): void {
668
- if (!this.#active) return;
669
-
670
- // Add a newline to move below the progress bar
671
- this.newLine();
672
-
673
- // Display the message
674
- this.write(message);
675
-
676
- // Redraw the progress bar
677
- this.update(0);
678
- }
679
-
680
- /**
681
- * Check if the progress bar is currently active.
682
- * @returns True if the progress bar is active, false otherwise.
683
- */
684
- public isActive(): boolean {
685
- return this.#active;
686
- }
687
-
688
- /**
689
- * Get the current progress value.
690
- * @returns The current progress value.
691
- */
692
- public getCurrent(): number {
693
- return this.#current;
694
- }
695
-
696
- /**
697
- * Get the total progress value.
698
- * @returns The total progress value.
699
- */
700
- public getTotal(): number {
701
- return this.#total;
702
- }
703
-
704
- /**
705
- * Get the current progress percentage.
706
- * @returns The current progress percentage (0-100).
707
- */
708
- public getPercentage(): number {
709
- return Math.min(100, Math.floor((this.#current / this.#total) * 100));
710
- }
711
- }
712
-
713
- /**
714
- * A class for displaying a spinner in the console for indeterminate operations.
715
- * @author Axel Nana <axel.nana@workbud.com>
716
- */
717
- export class CommandSpinner extends InteractsWithConsole {
718
- /**
719
- * Whether the spinner is currently active.
720
- */
721
- #active: boolean = false;
722
-
723
- /**
724
- * The title for the spinner.
725
- */
726
- #title: string = '';
727
-
728
- /**
729
- * The start time of the spinner.
730
- */
731
- #startTime: number = 0;
732
-
733
- /**
734
- * The interval ID for the spinner animation.
735
- */
736
- #intervalId: Timer | null = null;
737
-
738
- /**
739
- * The current frame of the spinner.
740
- */
741
- #currentFrame: number = 0;
742
-
743
- /**
744
- * The custom frames for the spinner animation.
745
- */
746
- readonly #frames: string[] = [
747
- '🕐',
748
- '🕑',
749
- '🕒',
750
- '🕓',
751
- '🕔',
752
- '🕕',
753
- '🕖',
754
- '🕗',
755
- '🕘',
756
- '🕙',
757
- '🕚',
758
- '🕛'
759
- ];
760
-
761
- /**
762
- * The delay between frames in milliseconds.
763
- */
764
- readonly #frameDelay: number = 80; // milliseconds between frames
765
-
766
- /**
767
- * Create a new spinner instance.
768
- * @param title Optional title for the spinner.
769
- * @param frames Optional custom frames for the spinner animation.
770
- * @param frameDelay Optional delay between frames in milliseconds.
771
- */
772
- constructor(title: string = 'Processing', frames?: string[], frameDelay?: number) {
773
- super();
774
-
775
- this.#title = title;
776
- if (frames) this.#frames = frames;
777
- if (frameDelay) this.#frameDelay = frameDelay;
778
-
779
- if (this.#active) return;
780
-
781
- this.#active = true;
782
- this.#startTime = Date.now();
783
- this.#currentFrame = 0;
784
-
785
- // Start the animation
786
- this.#intervalId = setInterval(() => {
787
- this.render();
788
- }, this.#frameDelay);
789
-
790
- // Initial render
791
- this.render();
792
- }
793
-
794
- /**
795
- * Update the spinner with a new title.
796
- * @param newTitle New title for the spinner.
797
- */
798
- public update(newTitle: string): void {
799
- if (!this.#active) return;
800
- this.#title = newTitle;
801
- }
802
-
803
- /**
804
- * Complete the spinner and reset its state.
805
- * @param message Optional completion message to display.
806
- * @param success Whether the operation was successful (affects the symbol shown).
807
- */
808
- public complete(message?: string, success: boolean = true): void {
809
- if (!this.#active) return;
810
-
811
- // Stop the animation
812
- if (this.#intervalId) {
813
- clearInterval(this.#intervalId);
814
- this.#intervalId = null;
815
- }
816
-
817
- // Calculate elapsed time
818
- const elapsedMs = Date.now() - this.#startTime;
819
- const elapsedSec = elapsedMs / 1000;
820
-
821
- // Clear the current line
822
- this.clearLine();
823
-
824
- // Determine completion symbol
825
- const time = this.formatTime(elapsedSec);
826
-
827
- // Write the completion message
828
- if (message) {
829
- this[success ? 'success' : 'error'](`${message} (${time})`);
830
- } else {
831
- this[success ? 'success' : 'error'](`${this.#title} completed in ${time}`);
832
- }
833
-
834
- // Reset spinner state
835
- this.#active = false;
836
- }
837
-
838
- /**
839
- * Fail the spinner and reset its state.
840
- * @param message Optional failure message to display.
841
- */
842
- public fail(message?: string): void {
843
- this.complete(message, false);
844
- }
845
-
846
- /**
847
- * Pause the spinner temporarily to display a message.
848
- * @param message The message to display.
849
- */
850
- public pause(message: string): void {
851
- if (!this.#active) return;
852
-
853
- // Temporarily stop the animation
854
- if (this.#intervalId) {
855
- clearInterval(this.#intervalId);
856
- this.#intervalId = null;
857
- }
858
-
859
- // Clear the current line
860
- this.clearLine();
861
-
862
- // Display the message
863
- this.write(message);
864
-
865
- // Restart the animation
866
- this.#intervalId = setInterval(() => {
867
- this.render();
868
- }, this.#frameDelay);
869
- }
870
-
871
- /**
872
- * Check if the spinner is currently active.
873
- * @returns True if the spinner is active, false otherwise.
874
- */
875
- public isActive(): boolean {
876
- return this.#active;
877
- }
878
-
879
- /**
880
- * Get the elapsed time since the spinner started.
881
- * @returns The elapsed time in seconds.
882
- */
883
- public getElapsedTime(): number {
884
- return (Date.now() - this.#startTime) / 1000;
885
- }
886
-
887
- /**
888
- * Render the current frame of the spinner.
889
- */
890
- private render(): void {
891
- if (!this.#active) return;
892
-
893
- // Calculate elapsed time
894
- const elapsedSec = this.getElapsedTime();
895
-
896
- // Get the current frame
897
- const frame = this.#frames[this.#currentFrame];
898
-
899
- // Increment frame counter
900
- this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
901
-
902
- // Clear the current line
903
- this.clearLine();
904
-
905
- // Write the spinner frame and title
906
- this.write(`${frame} ${this.#title} (${this.formatTime(elapsedSec)})`, false);
907
- }
908
- }