@sylphx/flow 3.16.0 → 3.17.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/CHANGELOG.md +15 -0
- package/assets/agents/builder.md +3 -1
- package/package.json +80 -79
- package/src/commands/flow/execute-v2.ts +44 -58
- package/src/commands/settings/checkbox-config.ts +18 -18
- package/src/commands/settings-command.ts +122 -144
- package/src/core/installers/mcp-installer.ts +42 -34
- package/src/core/target-manager.ts +13 -19
- package/src/core/upgrade-manager.ts +22 -19
- package/src/services/mcp-service.ts +33 -29
- package/src/services/target-installer.ts +28 -57
- package/src/utils/config/mcp-config.ts +31 -56
- package/src/utils/display/logger.ts +95 -172
- package/src/utils/prompt-helpers.ts +16 -5
- package/src/utils/prompts/index.ts +232 -0
- package/src/utils/target-selection.ts +38 -46
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized logging utility for Sylphx Flow
|
|
3
|
-
* Provides structured logging with
|
|
3
|
+
* Provides structured logging with Pino backend and pretty printing
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import chalk from 'chalk';
|
|
6
|
+
import pino from 'pino';
|
|
8
7
|
|
|
9
8
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
10
9
|
|
|
@@ -33,27 +32,6 @@ export interface LoggerConfig {
|
|
|
33
32
|
module?: string;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
37
|
-
debug: 0,
|
|
38
|
-
info: 1,
|
|
39
|
-
warn: 2,
|
|
40
|
-
error: 3,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const LEVEL_COLORS: Record<LogLevel, (text: string) => string> = {
|
|
44
|
-
debug: chalk.gray,
|
|
45
|
-
info: chalk.blue,
|
|
46
|
-
warn: chalk.yellow,
|
|
47
|
-
error: chalk.red,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const LEVEL_SYMBOLS: Record<LogLevel, string> = {
|
|
51
|
-
debug: '🔍',
|
|
52
|
-
info: 'ℹ',
|
|
53
|
-
warn: '⚠',
|
|
54
|
-
error: '✗',
|
|
55
|
-
};
|
|
56
|
-
|
|
57
35
|
/**
|
|
58
36
|
* Logger interface for dependency injection and testing
|
|
59
37
|
*/
|
|
@@ -76,6 +54,7 @@ export interface Logger {
|
|
|
76
54
|
interface LoggerState {
|
|
77
55
|
config: LoggerConfig;
|
|
78
56
|
context?: Record<string, unknown>;
|
|
57
|
+
pinoInstance: pino.Logger;
|
|
79
58
|
}
|
|
80
59
|
|
|
81
60
|
/**
|
|
@@ -86,6 +65,49 @@ interface CreateLoggerOptions {
|
|
|
86
65
|
context?: Record<string, unknown>;
|
|
87
66
|
}
|
|
88
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Map our log levels to Pino levels
|
|
70
|
+
*/
|
|
71
|
+
const LEVEL_MAP: Record<LogLevel, string> = {
|
|
72
|
+
debug: 'debug',
|
|
73
|
+
info: 'info',
|
|
74
|
+
warn: 'warn',
|
|
75
|
+
error: 'error',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a Pino instance based on configuration
|
|
80
|
+
*/
|
|
81
|
+
function createPinoInstance(config: LoggerConfig, context?: Record<string, unknown>): pino.Logger {
|
|
82
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
83
|
+
const usePretty = config.format === 'pretty' && !isProduction;
|
|
84
|
+
|
|
85
|
+
const pinoConfig: pino.LoggerOptions = {
|
|
86
|
+
level: config.level,
|
|
87
|
+
base: context ? { ...context } : undefined,
|
|
88
|
+
timestamp: config.includeTimestamp ? pino.stdTimeFunctions.isoTime : false,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (usePretty) {
|
|
92
|
+
// Use pino-pretty for development
|
|
93
|
+
return pino({
|
|
94
|
+
...pinoConfig,
|
|
95
|
+
transport: {
|
|
96
|
+
target: 'pino-pretty',
|
|
97
|
+
options: {
|
|
98
|
+
colorize: config.colors,
|
|
99
|
+
translateTime: 'HH:MM:ss',
|
|
100
|
+
ignore: 'pid,hostname',
|
|
101
|
+
messageFormat: '{msg}',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// JSON output for production or when format is 'json'
|
|
108
|
+
return pino(pinoConfig);
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
/**
|
|
90
112
|
* Create a logger instance with the specified configuration and context
|
|
91
113
|
*/
|
|
@@ -97,152 +119,19 @@ export function createLogger(options: Partial<LoggerConfig> | CreateLoggerOption
|
|
|
97
119
|
: (options as Partial<LoggerConfig>);
|
|
98
120
|
const initialContext = isOptionsStyle ? (options as CreateLoggerOptions).context : undefined;
|
|
99
121
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
...config,
|
|
108
|
-
},
|
|
109
|
-
context: initialContext,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Check if a log level should be output
|
|
114
|
-
*/
|
|
115
|
-
const shouldLog = (level: LogLevel): boolean => {
|
|
116
|
-
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[state.config.level];
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Create a log entry
|
|
121
|
-
*/
|
|
122
|
-
const createLogEntry = (
|
|
123
|
-
level: LogLevel,
|
|
124
|
-
message: string,
|
|
125
|
-
error?: Error,
|
|
126
|
-
additionalContext?: Record<string, unknown>
|
|
127
|
-
): LogEntry => {
|
|
128
|
-
const entry: LogEntry = {
|
|
129
|
-
id: randomUUID(),
|
|
130
|
-
timestamp: new Date().toISOString(),
|
|
131
|
-
level,
|
|
132
|
-
message,
|
|
133
|
-
module: state.context?.module,
|
|
134
|
-
function: state.context?.function,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Merge contexts
|
|
138
|
-
if (state.config.includeContext) {
|
|
139
|
-
entry.context = { ...state.context, ...additionalContext };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Add error information if provided
|
|
143
|
-
if (error) {
|
|
144
|
-
entry.error = {
|
|
145
|
-
name: error.name,
|
|
146
|
-
message: error.message,
|
|
147
|
-
stack: error.stack,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Add error code if it's a CLIError
|
|
151
|
-
if ('code' in error && typeof error.code === 'string') {
|
|
152
|
-
entry.error.code = error.code;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return entry;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Format a log entry for output
|
|
161
|
-
*/
|
|
162
|
-
const formatEntry = (entry: LogEntry): string => {
|
|
163
|
-
switch (state.config.format) {
|
|
164
|
-
case 'json':
|
|
165
|
-
return JSON.stringify(entry);
|
|
166
|
-
|
|
167
|
-
case 'simple': {
|
|
168
|
-
const levelStr = entry.level.toUpperCase().padEnd(5);
|
|
169
|
-
const moduleStr = entry.module ? `[${entry.module}] ` : '';
|
|
170
|
-
return `${levelStr} ${moduleStr}${entry.message}`;
|
|
171
|
-
}
|
|
172
|
-
default: {
|
|
173
|
-
const parts: string[] = [];
|
|
174
|
-
|
|
175
|
-
// Timestamp
|
|
176
|
-
if (state.config.includeTimestamp) {
|
|
177
|
-
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
178
|
-
parts.push(chalk.gray(time));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Level symbol and name
|
|
182
|
-
const colorFn = state.config.colors ? LEVEL_COLORS[entry.level] : (s: string) => s;
|
|
183
|
-
parts.push(
|
|
184
|
-
`${colorFn(LEVEL_SYMBOLS[entry.level])} ${colorFn(entry.level.toUpperCase().padEnd(5))}`
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
// Module
|
|
188
|
-
if (entry.module) {
|
|
189
|
-
parts.push(chalk.cyan(`[${entry.module}]`));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Function
|
|
193
|
-
if (entry.function) {
|
|
194
|
-
parts.push(chalk.gray(`${entry.function}()`));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Message
|
|
198
|
-
parts.push(entry.message);
|
|
199
|
-
|
|
200
|
-
let result = parts.join(' ');
|
|
201
|
-
|
|
202
|
-
// Context
|
|
203
|
-
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
204
|
-
const contextStr = JSON.stringify(entry.context, null, 2);
|
|
205
|
-
result += `\n${chalk.gray(' Context: ')}${chalk.gray(contextStr)}`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Error details
|
|
209
|
-
if (entry.error) {
|
|
210
|
-
result += `\n${chalk.red(' Error: ')}${chalk.red(entry.error.message)}`;
|
|
211
|
-
if (entry.error.code) {
|
|
212
|
-
result += `\n${chalk.red(' Code: ')}${chalk.red(entry.error.code)}`;
|
|
213
|
-
}
|
|
214
|
-
if (entry.error.stack) {
|
|
215
|
-
result += `\n${chalk.gray(entry.error.stack)}`;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
122
|
+
const defaultConfig: LoggerConfig = {
|
|
123
|
+
level: 'info',
|
|
124
|
+
format: 'pretty',
|
|
125
|
+
includeTimestamp: true,
|
|
126
|
+
includeContext: true,
|
|
127
|
+
colors: true,
|
|
128
|
+
...config,
|
|
222
129
|
};
|
|
223
130
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
level: LogLevel,
|
|
229
|
-
message: string,
|
|
230
|
-
error?: Error,
|
|
231
|
-
additionalContext?: Record<string, any>
|
|
232
|
-
): void => {
|
|
233
|
-
if (!shouldLog(level)) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const entry = createLogEntry(level, message, error, additionalContext);
|
|
238
|
-
const formatted = formatEntry(entry);
|
|
239
|
-
|
|
240
|
-
// Output to appropriate stream
|
|
241
|
-
if (level === 'error') {
|
|
242
|
-
console.error(formatted);
|
|
243
|
-
} else {
|
|
244
|
-
console.log(formatted);
|
|
245
|
-
}
|
|
131
|
+
const state: LoggerState = {
|
|
132
|
+
config: defaultConfig,
|
|
133
|
+
context: initialContext,
|
|
134
|
+
pinoInstance: createPinoInstance(defaultConfig, initialContext),
|
|
246
135
|
};
|
|
247
136
|
|
|
248
137
|
/**
|
|
@@ -267,41 +156,75 @@ export function createLogger(options: Partial<LoggerConfig> | CreateLoggerOption
|
|
|
267
156
|
*/
|
|
268
157
|
const setLevel = (level: LogLevel): void => {
|
|
269
158
|
state.config.level = level;
|
|
159
|
+
state.pinoInstance.level = level;
|
|
270
160
|
};
|
|
271
161
|
|
|
272
162
|
/**
|
|
273
163
|
* Update logger configuration
|
|
164
|
+
* Note: Recreates Pino instance when configuration changes
|
|
274
165
|
*/
|
|
275
166
|
const updateConfig = (config: Partial<LoggerConfig>): void => {
|
|
276
167
|
state.config = { ...state.config, ...config };
|
|
168
|
+
state.pinoInstance = createPinoInstance(state.config, state.context);
|
|
277
169
|
};
|
|
278
170
|
|
|
279
171
|
/**
|
|
280
172
|
* Debug level logging
|
|
281
173
|
*/
|
|
282
174
|
const debug = (message: string, context?: Record<string, unknown>): void => {
|
|
283
|
-
|
|
175
|
+
if (context && state.config.includeContext) {
|
|
176
|
+
state.pinoInstance.debug(context, message);
|
|
177
|
+
} else {
|
|
178
|
+
state.pinoInstance.debug(message);
|
|
179
|
+
}
|
|
284
180
|
};
|
|
285
181
|
|
|
286
182
|
/**
|
|
287
183
|
* Info level logging
|
|
288
184
|
*/
|
|
289
185
|
const info = (message: string, context?: Record<string, unknown>): void => {
|
|
290
|
-
|
|
186
|
+
if (context && state.config.includeContext) {
|
|
187
|
+
state.pinoInstance.info(context, message);
|
|
188
|
+
} else {
|
|
189
|
+
state.pinoInstance.info(message);
|
|
190
|
+
}
|
|
291
191
|
};
|
|
292
192
|
|
|
293
193
|
/**
|
|
294
194
|
* Warning level logging
|
|
295
195
|
*/
|
|
296
196
|
const warn = (message: string, context?: Record<string, unknown>): void => {
|
|
297
|
-
|
|
197
|
+
if (context && state.config.includeContext) {
|
|
198
|
+
state.pinoInstance.warn(context, message);
|
|
199
|
+
} else {
|
|
200
|
+
state.pinoInstance.warn(message);
|
|
201
|
+
}
|
|
298
202
|
};
|
|
299
203
|
|
|
300
204
|
/**
|
|
301
205
|
* Error level logging
|
|
302
206
|
*/
|
|
303
207
|
const error = (message: string, errorObj?: Error, context?: Record<string, unknown>): void => {
|
|
304
|
-
|
|
208
|
+
const errorContext: Record<string, unknown> = { ...context };
|
|
209
|
+
|
|
210
|
+
if (errorObj) {
|
|
211
|
+
errorContext.err = {
|
|
212
|
+
name: errorObj.name,
|
|
213
|
+
message: errorObj.message,
|
|
214
|
+
stack: errorObj.stack,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Add error code if it's a CLIError
|
|
218
|
+
if ('code' in errorObj && typeof errorObj.code === 'string') {
|
|
219
|
+
(errorContext.err as Record<string, unknown>).code = errorObj.code;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (Object.keys(errorContext).length > 0 && state.config.includeContext) {
|
|
224
|
+
state.pinoInstance.error(errorContext, message);
|
|
225
|
+
} else {
|
|
226
|
+
state.pinoInstance.error(message);
|
|
227
|
+
}
|
|
305
228
|
};
|
|
306
229
|
|
|
307
230
|
/**
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prompt Helpers
|
|
3
3
|
* Utilities for handling user prompts and error handling
|
|
4
|
+
*
|
|
5
|
+
* This module provides backward-compatible error handling that works with
|
|
6
|
+
* both legacy inquirer errors and the new Clack cancellation pattern.
|
|
4
7
|
*/
|
|
5
8
|
|
|
9
|
+
import { isCancel } from '@clack/prompts';
|
|
6
10
|
import { UserCancelledError } from './errors.js';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Check if error is a user cancellation (Ctrl+C or force closed)
|
|
10
|
-
*
|
|
14
|
+
* Supports both legacy inquirer errors and Clack cancellation symbols
|
|
15
|
+
* @param error - Error or symbol to check
|
|
11
16
|
* @returns True if error represents user cancellation
|
|
12
17
|
*/
|
|
13
18
|
export function isUserCancellation(error: unknown): boolean {
|
|
19
|
+
// Handle Clack cancellation symbol
|
|
20
|
+
if (isCancel(error)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle legacy inquirer errors
|
|
14
25
|
if (error === null || typeof error !== 'object') {
|
|
15
26
|
return false;
|
|
16
27
|
}
|
|
@@ -19,9 +30,9 @@ export function isUserCancellation(error: unknown): boolean {
|
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
/**
|
|
22
|
-
* Handle inquirer prompt errors with consistent error handling
|
|
33
|
+
* Handle inquirer/clack prompt errors with consistent error handling
|
|
23
34
|
* Throws UserCancelledError for user cancellations, re-throws other errors
|
|
24
|
-
* @param error - Error from
|
|
35
|
+
* @param error - Error from prompt
|
|
25
36
|
* @param message - Custom cancellation message
|
|
26
37
|
* @throws {UserCancelledError} If user cancelled the prompt
|
|
27
38
|
* @throws Original error if not a cancellation
|
|
@@ -34,8 +45,8 @@ export function handlePromptError(error: unknown, message: string): never {
|
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
/**
|
|
37
|
-
* Wrap
|
|
38
|
-
* @param promptFn - Function that returns a promise from
|
|
48
|
+
* Wrap a prompt with consistent error handling
|
|
49
|
+
* @param promptFn - Function that returns a promise from a prompt library
|
|
39
50
|
* @param errorMessage - Message to use if user cancels
|
|
40
51
|
* @returns Promise with the prompt result
|
|
41
52
|
* @throws {UserCancelledError} If user cancels the prompt
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Clack Prompts Wrapper
|
|
3
|
+
* Unified prompt utilities with consistent error handling and cancellation support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as clack from '@clack/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { UserCancelledError } from '../errors.js';
|
|
9
|
+
|
|
10
|
+
// Re-export clack utilities for convenience
|
|
11
|
+
export { cancel, group, intro, isCancel, log, note, outro } from '@clack/prompts';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface SelectOption<T> {
|
|
18
|
+
label: string;
|
|
19
|
+
value: T;
|
|
20
|
+
hint?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MultiselectOption<T> {
|
|
24
|
+
label: string;
|
|
25
|
+
value: T;
|
|
26
|
+
hint?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SpinnerInstance {
|
|
30
|
+
start: (message?: string) => void;
|
|
31
|
+
stop: (message?: string) => void;
|
|
32
|
+
message: (message: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Cancellation Handling
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handle cancellation by checking if result is a cancel symbol
|
|
41
|
+
* Throws UserCancelledError if cancelled
|
|
42
|
+
*/
|
|
43
|
+
export function handleCancellation<T>(
|
|
44
|
+
result: T | symbol,
|
|
45
|
+
message = 'Operation cancelled'
|
|
46
|
+
): asserts result is T {
|
|
47
|
+
if (clack.isCancel(result)) {
|
|
48
|
+
clack.cancel(message);
|
|
49
|
+
throw new UserCancelledError(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wrap a prompt result with automatic cancellation handling
|
|
55
|
+
*/
|
|
56
|
+
export function withCancellation<T>(result: T | symbol, message = 'Operation cancelled'): T {
|
|
57
|
+
handleCancellation(result, message);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Prompt Wrappers with Cancellation Handling
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Select prompt with automatic cancellation handling
|
|
67
|
+
*/
|
|
68
|
+
export async function promptSelect<T extends string | number | boolean>(options: {
|
|
69
|
+
message: string;
|
|
70
|
+
options: SelectOption<T>[];
|
|
71
|
+
initialValue?: T;
|
|
72
|
+
}): Promise<T> {
|
|
73
|
+
const result = await clack.select({
|
|
74
|
+
message: options.message,
|
|
75
|
+
options: options.options,
|
|
76
|
+
initialValue: options.initialValue,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
handleCancellation(result, 'Selection cancelled');
|
|
80
|
+
return result as T;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Multiselect prompt with automatic cancellation handling
|
|
85
|
+
*/
|
|
86
|
+
export async function promptMultiselect<T extends string | number | boolean>(options: {
|
|
87
|
+
message: string;
|
|
88
|
+
options: MultiselectOption<T>[];
|
|
89
|
+
initialValues?: T[];
|
|
90
|
+
required?: boolean;
|
|
91
|
+
}): Promise<T[]> {
|
|
92
|
+
const result = await clack.multiselect({
|
|
93
|
+
message: options.message,
|
|
94
|
+
options: options.options,
|
|
95
|
+
initialValues: options.initialValues,
|
|
96
|
+
required: options.required ?? false,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
handleCancellation(result, 'Selection cancelled');
|
|
100
|
+
return result as T[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Confirm prompt with automatic cancellation handling
|
|
105
|
+
*/
|
|
106
|
+
export async function promptConfirm(options: {
|
|
107
|
+
message: string;
|
|
108
|
+
initialValue?: boolean;
|
|
109
|
+
}): Promise<boolean> {
|
|
110
|
+
const result = await clack.confirm({
|
|
111
|
+
message: options.message,
|
|
112
|
+
initialValue: options.initialValue ?? true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
handleCancellation(result, 'Confirmation cancelled');
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Text input prompt with automatic cancellation handling
|
|
121
|
+
*/
|
|
122
|
+
export async function promptText(options: {
|
|
123
|
+
message: string;
|
|
124
|
+
placeholder?: string;
|
|
125
|
+
defaultValue?: string;
|
|
126
|
+
validate?: (value: string) => string | undefined;
|
|
127
|
+
}): Promise<string> {
|
|
128
|
+
const result = await clack.text({
|
|
129
|
+
message: options.message,
|
|
130
|
+
placeholder: options.placeholder,
|
|
131
|
+
defaultValue: options.defaultValue,
|
|
132
|
+
validate: options.validate,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
handleCancellation(result, 'Input cancelled');
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Password input prompt with automatic cancellation handling
|
|
141
|
+
*/
|
|
142
|
+
export async function promptPassword(options: {
|
|
143
|
+
message: string;
|
|
144
|
+
mask?: string;
|
|
145
|
+
validate?: (value: string) => string | undefined;
|
|
146
|
+
}): Promise<string> {
|
|
147
|
+
const result = await clack.password({
|
|
148
|
+
message: options.message,
|
|
149
|
+
mask: options.mask ?? '*',
|
|
150
|
+
validate: options.validate,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
handleCancellation(result, 'Password input cancelled');
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Spinner Wrapper
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create a spinner instance with consistent API
|
|
163
|
+
* Wraps Clack's spinner with additional convenience methods
|
|
164
|
+
*/
|
|
165
|
+
export function createSpinner(): SpinnerInstance {
|
|
166
|
+
const s = clack.spinner();
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
start: (message?: string) => s.start(message),
|
|
170
|
+
stop: (message?: string) => s.stop(message),
|
|
171
|
+
message: (message: string) => s.message(message),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Run an async operation with a spinner
|
|
177
|
+
*/
|
|
178
|
+
export async function withSpinner<T>(
|
|
179
|
+
message: string,
|
|
180
|
+
fn: () => Promise<T>,
|
|
181
|
+
options?: {
|
|
182
|
+
successMessage?: string | ((result: T) => string);
|
|
183
|
+
errorMessage?: string | ((error: Error) => string);
|
|
184
|
+
}
|
|
185
|
+
): Promise<T> {
|
|
186
|
+
const s = clack.spinner();
|
|
187
|
+
s.start(message);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const result = await fn();
|
|
191
|
+
const successMsg =
|
|
192
|
+
typeof options?.successMessage === 'function'
|
|
193
|
+
? options.successMessage(result)
|
|
194
|
+
: (options?.successMessage ?? message);
|
|
195
|
+
s.stop(chalk.green(`✓ ${successMsg}`));
|
|
196
|
+
return result;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const errorMsg =
|
|
199
|
+
typeof options?.errorMessage === 'function'
|
|
200
|
+
? options.errorMessage(error as Error)
|
|
201
|
+
: (options?.errorMessage ?? `Failed: ${message}`);
|
|
202
|
+
s.stop(chalk.red(`✗ ${errorMsg}`));
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Group Prompts with Cancellation
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Group multiple prompts together with shared cancellation handling
|
|
213
|
+
* If any prompt is cancelled, the entire group is cancelled
|
|
214
|
+
*/
|
|
215
|
+
export async function promptGroup<T extends Record<string, unknown>>(
|
|
216
|
+
prompts: {
|
|
217
|
+
[K in keyof T]: () => Promise<T[K] | symbol>;
|
|
218
|
+
},
|
|
219
|
+
options?: {
|
|
220
|
+
onCancel?: () => void;
|
|
221
|
+
}
|
|
222
|
+
): Promise<T> {
|
|
223
|
+
const result = await clack.group(prompts, {
|
|
224
|
+
onCancel: () => {
|
|
225
|
+
options?.onCancel?.();
|
|
226
|
+
clack.cancel('Operation cancelled');
|
|
227
|
+
throw new UserCancelledError('Operation cancelled');
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return result as T;
|
|
232
|
+
}
|