movehat 0.1.1 → 0.1.2

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 (71) hide show
  1. package/dist/commands/compile.d.ts +13 -0
  2. package/dist/commands/compile.d.ts.map +1 -1
  3. package/dist/commands/compile.js +31 -11
  4. package/dist/commands/compile.js.map +1 -1
  5. package/dist/commands/fork/create.d.ts +20 -1
  6. package/dist/commands/fork/create.d.ts.map +1 -1
  7. package/dist/commands/fork/create.js +54 -20
  8. package/dist/commands/fork/create.js.map +1 -1
  9. package/dist/commands/fork/list.d.ts +12 -1
  10. package/dist/commands/fork/list.d.ts.map +1 -1
  11. package/dist/commands/fork/list.js +50 -22
  12. package/dist/commands/fork/list.js.map +1 -1
  13. package/dist/commands/init.d.ts +19 -0
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +64 -29
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/update.d.ts +11 -1
  18. package/dist/commands/update.d.ts.map +1 -1
  19. package/dist/commands/update.js +44 -18
  20. package/dist/commands/update.js.map +1 -1
  21. package/dist/helpers/banner.d.ts +17 -0
  22. package/dist/helpers/banner.d.ts.map +1 -1
  23. package/dist/helpers/banner.js +38 -23
  24. package/dist/helpers/banner.js.map +1 -1
  25. package/dist/helpers/version-check.d.ts +12 -1
  26. package/dist/helpers/version-check.d.ts.map +1 -1
  27. package/dist/helpers/version-check.js +17 -7
  28. package/dist/helpers/version-check.js.map +1 -1
  29. package/dist/ui/colors.d.ts +77 -0
  30. package/dist/ui/colors.d.ts.map +1 -0
  31. package/dist/ui/colors.js +121 -0
  32. package/dist/ui/colors.js.map +1 -0
  33. package/dist/ui/formatters.d.ts +171 -0
  34. package/dist/ui/formatters.d.ts.map +1 -0
  35. package/dist/ui/formatters.js +186 -0
  36. package/dist/ui/formatters.js.map +1 -0
  37. package/dist/ui/index.d.ts +58 -0
  38. package/dist/ui/index.d.ts.map +1 -0
  39. package/dist/ui/index.js +60 -0
  40. package/dist/ui/index.js.map +1 -0
  41. package/dist/ui/logger.d.ts +160 -0
  42. package/dist/ui/logger.d.ts.map +1 -0
  43. package/dist/ui/logger.js +206 -0
  44. package/dist/ui/logger.js.map +1 -0
  45. package/dist/ui/spinner.d.ts +106 -0
  46. package/dist/ui/spinner.d.ts.map +1 -0
  47. package/dist/ui/spinner.js +120 -0
  48. package/dist/ui/spinner.js.map +1 -0
  49. package/dist/ui/symbols.d.ts +50 -0
  50. package/dist/ui/symbols.d.ts.map +1 -0
  51. package/dist/ui/symbols.js +64 -0
  52. package/dist/ui/symbols.js.map +1 -0
  53. package/dist/ui/table.d.ts +67 -0
  54. package/dist/ui/table.d.ts.map +1 -0
  55. package/dist/ui/table.js +143 -0
  56. package/dist/ui/table.js.map +1 -0
  57. package/package.json +8 -1
  58. package/src/commands/compile.ts +32 -11
  59. package/src/commands/fork/create.ts +59 -20
  60. package/src/commands/fork/list.ts +52 -22
  61. package/src/commands/init.ts +111 -74
  62. package/src/commands/update.ts +52 -19
  63. package/src/helpers/banner.ts +45 -29
  64. package/src/helpers/version-check.ts +20 -8
  65. package/src/ui/colors.ts +141 -0
  66. package/src/ui/formatters.ts +246 -0
  67. package/src/ui/index.ts +62 -0
  68. package/src/ui/logger.ts +226 -0
  69. package/src/ui/spinner.ts +171 -0
  70. package/src/ui/symbols.ts +74 -0
  71. package/src/ui/table.ts +191 -0
@@ -0,0 +1,246 @@
1
+ import { colors } from './colors.js';
2
+ import { symbols } from './symbols.js';
3
+
4
+ /**
5
+ * Border color options for boxes
6
+ */
7
+ export type BorderColor = 'dim' | 'primary' | 'success' | 'error' | 'warning';
8
+
9
+ /**
10
+ * Box formatting options
11
+ */
12
+ export interface BoxOptions {
13
+ /** Padding inside the box (default: 1) */
14
+ padding?: number;
15
+ /** Margin outside the box (default: 0) */
16
+ margin?: number;
17
+ /** Border color (default: 'dim') */
18
+ borderColor?: BorderColor;
19
+ }
20
+
21
+ /**
22
+ * Create a box around text with Unicode borders
23
+ *
24
+ * @param content - Text content to box (supports multiline)
25
+ * @param options - Box formatting options
26
+ * @returns Formatted box string
27
+ *
28
+ * @example
29
+ * const message = box('Update available: 1.0.0 → 1.1.0');
30
+ * console.log(message);
31
+ * // ┌────────────────────────────────┐
32
+ * // │ Update available: 1.0.0 → 1.1.0│
33
+ * // └────────────────────────────────┘
34
+ *
35
+ * @example
36
+ * const warning = box(
37
+ * 'This feature is deprecated\nUse the new API instead',
38
+ * { borderColor: 'warning', padding: 2 }
39
+ * );
40
+ */
41
+ export const box = (
42
+ content: string,
43
+ options: BoxOptions = {}
44
+ ): string => {
45
+ const { padding = 1, margin = 0, borderColor = 'dim' } = options;
46
+
47
+ const lines = content.split('\n');
48
+ const maxWidth = Math.max(...lines.map(l => l.length));
49
+ const innerWidth = maxWidth + padding * 2;
50
+
51
+ const marginStr = ' '.repeat(margin);
52
+ const paddingStr = ' '.repeat(padding);
53
+
54
+ const colorFn = colors[borderColor] || colors.dim;
55
+
56
+ const top = marginStr + colorFn(symbols.boxTopLeft + symbols.boxTop.repeat(innerWidth) + symbols.boxTopRight);
57
+ const bottom = marginStr + colorFn(symbols.boxBottomLeft + symbols.boxBottom.repeat(innerWidth) + symbols.boxBottomRight);
58
+
59
+ const middle = lines.map(line => {
60
+ const padded = line.padEnd(maxWidth);
61
+ return marginStr + colorFn(symbols.boxLeft) + paddingStr + padded + paddingStr + colorFn(symbols.boxRight);
62
+ });
63
+
64
+ return [top, ...middle, bottom].join('\n');
65
+ };
66
+
67
+ /**
68
+ * List formatting options
69
+ */
70
+ export interface ListOptions {
71
+ /** Number of spaces to indent (default: 0) */
72
+ indent?: number;
73
+ /** Starting number for numbered lists (default: 1) */
74
+ startFrom?: number;
75
+ /** Custom symbol for bullet lists */
76
+ symbol?: string;
77
+ }
78
+
79
+ /**
80
+ * Create a numbered list
81
+ *
82
+ * @param items - Array of items to list
83
+ * @param options - List formatting options
84
+ * @returns Formatted numbered list string
85
+ *
86
+ * @example
87
+ * const steps = numberedList([
88
+ * 'cd my-project',
89
+ * 'npm install',
90
+ * 'npm test'
91
+ * ]);
92
+ * console.log(steps);
93
+ * // 1. cd my-project
94
+ * // 2. npm install
95
+ * // 3. npm test
96
+ */
97
+ export const numberedList = (
98
+ items: string[],
99
+ options: ListOptions = {}
100
+ ): string => {
101
+ const { indent = 0, startFrom = 1 } = options;
102
+ const prefix = ' '.repeat(indent);
103
+
104
+ return items
105
+ .map((item, idx) => `${prefix}${startFrom + idx}. ${item}`)
106
+ .join('\n');
107
+ };
108
+
109
+ /**
110
+ * Create a bulleted list
111
+ *
112
+ * @param items - Array of items to list
113
+ * @param options - List formatting options
114
+ * @returns Formatted bulleted list string
115
+ *
116
+ * @example
117
+ * const features = bulletList([
118
+ * 'Feature A',
119
+ * 'Feature B',
120
+ * 'Feature C'
121
+ * ]);
122
+ * console.log(features);
123
+ * // • Feature A
124
+ * // • Feature B
125
+ * // • Feature C
126
+ *
127
+ * @example
128
+ * const tasks = bulletList(
129
+ * ['Task 1', 'Task 2'],
130
+ * { indent: 2, symbol: '→' }
131
+ * );
132
+ */
133
+ export const bulletList = (
134
+ items: string[],
135
+ options: ListOptions = {}
136
+ ): string => {
137
+ const { indent = 0, symbol = symbols.bullet } = options;
138
+ const prefix = ' '.repeat(indent);
139
+
140
+ return items
141
+ .map(item => `${prefix}${symbol} ${item}`)
142
+ .join('\n');
143
+ };
144
+
145
+ /**
146
+ * Indent text block by a specified number of spaces
147
+ *
148
+ * @param text - Text to indent (supports multiline)
149
+ * @param spaces - Number of spaces to indent
150
+ * @returns Indented text
151
+ *
152
+ * @example
153
+ * const code = 'function test() {\n return true;\n}';
154
+ * console.log(indent(code, 4));
155
+ * // function test() {
156
+ * // return true;
157
+ * // }
158
+ */
159
+ export const indent = (text: string, spaces: number): string => {
160
+ const prefix = ' '.repeat(spaces);
161
+ return text.split('\n').map(line => prefix + line).join('\n');
162
+ };
163
+
164
+ /**
165
+ * Create a horizontal divider line
166
+ *
167
+ * @param width - Width of the divider (default: 60)
168
+ * @param char - Character to use (default: symbols.line)
169
+ * @returns Formatted divider string
170
+ *
171
+ * @example
172
+ * console.log(divider());
173
+ * // ────────────────────────────────────────────────────────────
174
+ *
175
+ * @example
176
+ * console.log(divider(40, '='));
177
+ * // ========================================
178
+ */
179
+ export const divider = (
180
+ width: number = 60,
181
+ char: string = symbols.line
182
+ ): string => {
183
+ return colors.dim(char.repeat(width));
184
+ };
185
+
186
+ /**
187
+ * Format file path with dimmed parent directories
188
+ * Highlights the filename while dimming the directory path
189
+ *
190
+ * @param filePath - Full file path
191
+ * @returns Formatted path string
192
+ *
193
+ * @example
194
+ * console.log(formatPath('/Users/name/project/src/file.ts'));
195
+ * // /Users/name/project/src/file.ts
196
+ * // (where the directory is dimmed and filename is bright)
197
+ */
198
+ export const formatPath = (filePath: string): string => {
199
+ const parts = filePath.split('/');
200
+ const fileName = parts.pop() || '';
201
+ const dirPath = parts.join('/');
202
+
203
+ return colors.dim(dirPath + '/') + fileName;
204
+ };
205
+
206
+ /**
207
+ * Create a section header with divider
208
+ * Combines a bold title with a horizontal line
209
+ *
210
+ * @param title - Section title
211
+ * @param options - Formatting options
212
+ * @returns Formatted section header
213
+ *
214
+ * @example
215
+ * console.log(sectionHeader('Fork Details'));
216
+ * //
217
+ * // Fork Details
218
+ * // ────────────────────────────────────────────────────────────
219
+ */
220
+ export const sectionHeader = (
221
+ title: string,
222
+ options: { width?: number; char?: string } = {}
223
+ ): string => {
224
+ const { width = 60, char = symbols.line } = options;
225
+ return `\n${colors.brandBright(title)}\n${divider(width, char)}`;
226
+ };
227
+
228
+ /**
229
+ * Format a command for display with dimmed prompt
230
+ * Useful for showing example commands to users
231
+ *
232
+ * @param command - Command string
233
+ * @returns Formatted command with $ prompt
234
+ *
235
+ * @example
236
+ * console.log(formatCommand('movehat compile'));
237
+ * // $ movehat compile
238
+ * // (where $ is dimmed)
239
+ *
240
+ * @example
241
+ * console.log(formatCommand('npm install'));
242
+ * // $ npm install
243
+ */
244
+ export const formatCommand = (command: string): string => {
245
+ return `${colors.dim('$')} ${command}`;
246
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Movehat UI Module
3
+ *
4
+ * Provides a comprehensive set of UI utilities for the CLI including:
5
+ * - Colors and gradients (picocolors wrapper)
6
+ * - Cross-platform symbols (figures wrapper)
7
+ * - Structured logging system
8
+ * - Spinners for async operations (ora wrapper)
9
+ * - Table formatting (cli-table3 wrapper)
10
+ * - Text formatting utilities
11
+ *
12
+ * @example
13
+ * // Named imports (recommended)
14
+ * import { logger, spinner, createTable } from './ui/index.js';
15
+ *
16
+ * logger.info('Starting compilation...');
17
+ * const spin = spinner({ text: 'Compiling...' });
18
+ * // ... async work ...
19
+ * spin.succeed('Compilation complete!');
20
+ *
21
+ * @example
22
+ * // Namespace import
23
+ * import { ui } from './ui/index.js';
24
+ *
25
+ * ui.logger.info('Starting...');
26
+ * ui.logger.success('Done!');
27
+ */
28
+
29
+ // Re-export everything from submodules for named imports
30
+ export * from './colors.js';
31
+ export * from './symbols.js';
32
+ export * from './logger.js';
33
+ export * from './spinner.js';
34
+ export * from './table.js';
35
+ export * from './formatters.js';
36
+
37
+ // Also export as namespace for convenience
38
+ import * as _colors from './colors.js';
39
+ import * as _symbols from './symbols.js';
40
+ import * as _logger from './logger.js';
41
+ import * as _spinner from './spinner.js';
42
+ import * as _table from './table.js';
43
+ import * as _formatters from './formatters.js';
44
+
45
+ /**
46
+ * Unified UI namespace
47
+ * Provides all UI utilities in a single namespace object
48
+ *
49
+ * @example
50
+ * import { ui } from './ui/index.js';
51
+ *
52
+ * ui.logger.info('Processing...');
53
+ * ui.logger.success('Done!');
54
+ */
55
+ export const ui = {
56
+ colors: _colors,
57
+ symbols: _symbols,
58
+ logger: _logger,
59
+ spinner: _spinner,
60
+ table: _table,
61
+ formatters: _formatters,
62
+ };
@@ -0,0 +1,226 @@
1
+ import { colors } from './colors.js';
2
+ import { coloredSymbol, symbols } from './symbols.js';
3
+
4
+ /**
5
+ * Available log levels
6
+ */
7
+ export type LogLevel = 'info' | 'success' | 'error' | 'warning' | 'debug';
8
+
9
+ /**
10
+ * Logger configuration options
11
+ */
12
+ export interface LoggerConfig {
13
+ /** Suppress all output when true */
14
+ silent?: boolean;
15
+ /** Minimum log level to display */
16
+ level?: LogLevel;
17
+ /** Include timestamps in log messages */
18
+ timestamp?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Internal logger state
23
+ */
24
+ let config: LoggerConfig = {
25
+ silent: false,
26
+ level: 'info',
27
+ timestamp: false,
28
+ };
29
+
30
+ /**
31
+ * Configure logger globally
32
+ *
33
+ * @param newConfig - Partial configuration to merge with current config
34
+ *
35
+ * @example
36
+ * // Silence all logs for testing
37
+ * configureLogger({ silent: true });
38
+ *
39
+ * // Enable timestamps
40
+ * configureLogger({ timestamp: true });
41
+ */
42
+ export const configureLogger = (newConfig: Partial<LoggerConfig>): void => {
43
+ config = { ...config, ...newConfig };
44
+ };
45
+
46
+ /**
47
+ * Format message with optional indentation
48
+ */
49
+ const formatMessage = (message: string, indent: number = 0): string => {
50
+ const prefix = ' '.repeat(indent);
51
+ return message.split('\n').map(line => prefix + line).join('\n');
52
+ };
53
+
54
+ /**
55
+ * Info message (cyan)
56
+ * Use for general information and status updates
57
+ *
58
+ * @param message - Message to log
59
+ * @param indent - Number of spaces to indent (default: 0)
60
+ *
61
+ * @example
62
+ * logger.info('Starting compilation...');
63
+ * logger.info('Network: testnet', 2);
64
+ */
65
+ export const info = (message: string, indent: number = 0): void => {
66
+ if (config.silent) return;
67
+ const formatted = formatMessage(message, indent);
68
+ console.log(`${coloredSymbol('info')} ${formatted}`);
69
+ };
70
+
71
+ /**
72
+ * Success message (green)
73
+ * Use for completed operations and positive outcomes
74
+ *
75
+ * @param message - Message to log
76
+ * @param indent - Number of spaces to indent (default: 0)
77
+ *
78
+ * @example
79
+ * logger.success('Compilation finished!');
80
+ * logger.success('All tests passed', 2);
81
+ */
82
+ export const success = (message: string, indent: number = 0): void => {
83
+ if (config.silent) return;
84
+ const formatted = formatMessage(message, indent);
85
+ console.log(`${coloredSymbol('success')} ${formatted}`);
86
+ };
87
+
88
+ /**
89
+ * Error message (red)
90
+ * Use for errors and failures
91
+ *
92
+ * @param message - Message to log
93
+ * @param indent - Number of spaces to indent (default: 0)
94
+ *
95
+ * @example
96
+ * logger.error('Compilation failed');
97
+ * logger.error('File not found: config.ts', 2);
98
+ */
99
+ export const error = (message: string, indent: number = 0): void => {
100
+ if (config.silent) return;
101
+ const formatted = formatMessage(message, indent);
102
+ console.error(`${coloredSymbol('error')} ${formatted}`);
103
+ };
104
+
105
+ /**
106
+ * Warning message (yellow)
107
+ * Use for warnings and deprecated features
108
+ *
109
+ * @param message - Message to log
110
+ * @param indent - Number of spaces to indent (default: 0)
111
+ *
112
+ * @example
113
+ * logger.warning('Deprecated API used');
114
+ * logger.warning('This feature will be removed in v2.0', 2);
115
+ */
116
+ export const warning = (message: string, indent: number = 0): void => {
117
+ if (config.silent) return;
118
+ const formatted = formatMessage(message, indent);
119
+ console.warn(`${coloredSymbol('warning')} ${formatted}`);
120
+ };
121
+
122
+ /**
123
+ * Plain message without symbol
124
+ * Use for continuation lines or when symbol is not appropriate
125
+ *
126
+ * @param message - Message to log
127
+ *
128
+ * @example
129
+ * logger.info('Fork Details:');
130
+ * logger.plain(' Chain ID: 126');
131
+ * logger.plain(' Network: testnet');
132
+ */
133
+ export const plain = (message: string): void => {
134
+ if (config.silent) return;
135
+ console.log(message);
136
+ };
137
+
138
+ /**
139
+ * Empty line
140
+ * Use for visual spacing between sections
141
+ *
142
+ * @example
143
+ * logger.success('Build complete');
144
+ * logger.newline();
145
+ * logger.info('Next steps:');
146
+ */
147
+ export const newline = (): void => {
148
+ if (config.silent) return;
149
+ console.log();
150
+ };
151
+
152
+ /**
153
+ * Section header (bold, brand color)
154
+ * Use for major section dividers
155
+ *
156
+ * @param title - Section title
157
+ *
158
+ * @example
159
+ * logger.section('Fork Details');
160
+ * logger.kv('Chain ID', '126', 2);
161
+ * logger.kv('Network', 'testnet', 2);
162
+ */
163
+ export const section = (title: string): void => {
164
+ if (config.silent) return;
165
+ console.log(`\n${colors.brandBright(title)}`);
166
+ };
167
+
168
+ /**
169
+ * Key-value pair
170
+ * Use for displaying structured data
171
+ *
172
+ * @param key - The key/label
173
+ * @param value - The value
174
+ * @param indent - Number of spaces to indent (default: 0)
175
+ *
176
+ * @example
177
+ * logger.kv('Network', 'testnet', 2);
178
+ * logger.kv('Chain ID', '126', 2);
179
+ */
180
+ export const kv = (key: string, value: string, indent: number = 0): void => {
181
+ if (config.silent) return;
182
+ const prefix = ' '.repeat(indent);
183
+ console.log(`${prefix}${colors.dim(key)}: ${value}`);
184
+ };
185
+
186
+ /**
187
+ * List item with bullet
188
+ * Use for lists and enumerated items
189
+ *
190
+ * @param text - Item text
191
+ * @param indent - Number of spaces to indent (default: 0)
192
+ *
193
+ * @example
194
+ * logger.info('Next steps:');
195
+ * logger.item('cd my-project', 2);
196
+ * logger.item('npm install', 2);
197
+ * logger.item('npm test', 2);
198
+ */
199
+ export const item = (text: string, indent: number = 0): void => {
200
+ if (config.silent) return;
201
+ const prefix = ' '.repeat(indent);
202
+ console.log(`${prefix}${symbols.bullet} ${text}`);
203
+ };
204
+
205
+ /**
206
+ * Logger namespace export
207
+ * Provides all logging functions in a single namespace
208
+ *
209
+ * @example
210
+ * import { logger } from './ui/index.js';
211
+ *
212
+ * logger.info('Starting...');
213
+ * logger.success('Done!');
214
+ */
215
+ export const logger = {
216
+ configure: configureLogger,
217
+ info,
218
+ success,
219
+ error,
220
+ warning,
221
+ plain,
222
+ newline,
223
+ section,
224
+ kv,
225
+ item,
226
+ };
@@ -0,0 +1,171 @@
1
+ import ora, { type Ora, type Options as OraOptions } from 'ora';
2
+ import { shouldUseColor } from './colors.js';
3
+
4
+ /**
5
+ * Spinner color options
6
+ */
7
+ export type SpinnerColor = 'yellow' | 'green' | 'cyan' | 'red' | 'blue' | 'magenta' | 'white' | 'gray';
8
+
9
+ /**
10
+ * Spinner configuration options
11
+ */
12
+ export interface SpinnerOptions {
13
+ /** Text to display next to the spinner */
14
+ text: string;
15
+ /** Spinner color (default: 'yellow' for Movehat brand) */
16
+ color?: SpinnerColor;
17
+ /** Spinner animation type (default: 'dots') */
18
+ spinner?: OraOptions['spinner'];
19
+ /** Number of spaces to indent (default: 0) */
20
+ indent?: number;
21
+ }
22
+
23
+ /**
24
+ * Create and start a spinner
25
+ * Automatically disabled in non-TTY environments (CI, pipes)
26
+ *
27
+ * @param options - Spinner configuration
28
+ * @returns Ora spinner instance
29
+ *
30
+ * @example
31
+ * const spin = spinner({ text: 'Compiling contracts...' });
32
+ * await longRunningTask();
33
+ * spin.succeed('Compilation complete!');
34
+ *
35
+ * @example
36
+ * const spin = spinner({ text: 'Fetching data...', color: 'cyan' });
37
+ * try {
38
+ * await fetchData();
39
+ * spin.succeed('Data fetched!');
40
+ * } catch (error) {
41
+ * spin.fail('Failed to fetch data');
42
+ * }
43
+ */
44
+ export const spinner = (options: SpinnerOptions): Ora => {
45
+ const { text, color = 'yellow', spinner = 'dots', indent = 0 } = options;
46
+
47
+ const prefixSpaces = ' '.repeat(indent);
48
+
49
+ const oraOptions: OraOptions = {
50
+ text: prefixSpaces + text,
51
+ color,
52
+ spinner,
53
+ // Disable spinner if not TTY (CI environments, piped output)
54
+ isEnabled: shouldUseColor() && Boolean(process.stdout.isTTY),
55
+ };
56
+
57
+ return ora(oraOptions).start();
58
+ };
59
+
60
+ /**
61
+ * Execute async task with spinner
62
+ * Handles success/error automatically and always cleans up
63
+ *
64
+ * @param startText - Initial spinner text
65
+ * @param task - Async function to execute
66
+ * @param successText - Text to show on success (optional, defaults to startText without '...')
67
+ * @param errorText - Text to show on error (optional, defaults to error message)
68
+ * @param indent - Number of spaces to indent (default: 0)
69
+ * @returns Promise resolving to task result
70
+ *
71
+ * @example
72
+ * const data = await withSpinner(
73
+ * 'Fetching network data...',
74
+ * async () => await fetchData(),
75
+ * 'Data fetched successfully'
76
+ * );
77
+ *
78
+ * @example
79
+ * await withSpinner(
80
+ * 'Creating fork...',
81
+ * async () => await createFork(),
82
+ * 'Fork created!',
83
+ * 'Failed to create fork'
84
+ * );
85
+ */
86
+ export const withSpinner = async <T>(
87
+ startText: string,
88
+ task: () => Promise<T>,
89
+ successText?: string,
90
+ errorText?: string,
91
+ indent: number = 0
92
+ ): Promise<T> => {
93
+ const spin = spinner({ text: startText, indent });
94
+
95
+ try {
96
+ const result = await task();
97
+ spin.succeed(successText || startText.replace(/\.\.\.?$/, ''));
98
+ return result;
99
+ } catch (error) {
100
+ const errMsg = error instanceof Error ? error.message : String(error);
101
+ spin.fail(errorText || `Failed: ${errMsg}`);
102
+ throw error;
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Spinner chain for sequential operations
108
+ * Manages multiple spinners in sequence
109
+ */
110
+ export interface SpinnerChain {
111
+ /**
112
+ * Add and execute a step in the chain
113
+ */
114
+ add<T>(text: string, task: () => Promise<T>, indent?: number): Promise<T>;
115
+
116
+ /**
117
+ * Complete the chain (cleanup)
118
+ */
119
+ complete(): void;
120
+ }
121
+
122
+ /**
123
+ * Create a sequential spinner chain
124
+ * Useful for multi-step processes like initialization
125
+ *
126
+ * @returns SpinnerChain interface
127
+ *
128
+ * @example
129
+ * const steps = createSpinnerChain();
130
+ *
131
+ * await steps.add('Creating directories...', async () => {
132
+ * await mkdir(projectPath);
133
+ * });
134
+ *
135
+ * await steps.add('Copying templates...', async () => {
136
+ * await copyFiles();
137
+ * });
138
+ *
139
+ * await steps.add('Installing dependencies...', async () => {
140
+ * await npmInstall();
141
+ * });
142
+ *
143
+ * steps.complete();
144
+ */
145
+ export const createSpinnerChain = (): SpinnerChain => {
146
+ let currentSpinner: Ora | null = null;
147
+
148
+ return {
149
+ async add<T>(
150
+ text: string,
151
+ task: () => Promise<T>,
152
+ indent: number = 0
153
+ ): Promise<T> {
154
+ currentSpinner = spinner({ text, indent });
155
+ try {
156
+ const result = await task();
157
+ currentSpinner.succeed();
158
+ return result;
159
+ } catch (error) {
160
+ currentSpinner.fail();
161
+ throw error;
162
+ }
163
+ },
164
+
165
+ complete() {
166
+ if (currentSpinner) {
167
+ currentSpinner.stop();
168
+ }
169
+ },
170
+ };
171
+ };