@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Prompts for Bueno CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides interactive prompts for user input
|
|
5
|
+
* Falls back to simple input for non-TTY environments
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import { colors } from './console';
|
|
10
|
+
|
|
11
|
+
export interface PromptOptions {
|
|
12
|
+
default?: string;
|
|
13
|
+
validate?: (value: string) => boolean | string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SelectOptions<T = string> {
|
|
17
|
+
default?: T;
|
|
18
|
+
pageSize?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ConfirmOptions {
|
|
22
|
+
default?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MultiSelectOptions<T = string> {
|
|
26
|
+
default?: T[];
|
|
27
|
+
pageSize?: number;
|
|
28
|
+
min?: number;
|
|
29
|
+
max?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if running in interactive mode
|
|
34
|
+
*/
|
|
35
|
+
export function isInteractive(): boolean {
|
|
36
|
+
return !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a readline interface
|
|
41
|
+
*/
|
|
42
|
+
function createRL(): readline.ReadLine {
|
|
43
|
+
return readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prompt for text input
|
|
51
|
+
*/
|
|
52
|
+
export async function prompt(
|
|
53
|
+
message: string,
|
|
54
|
+
options: PromptOptions = {},
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
const defaultValue = options.default;
|
|
57
|
+
const promptText = defaultValue
|
|
58
|
+
? `${colors.cyan('?')} ${message} ${colors.dim(`(${defaultValue})`)}: `
|
|
59
|
+
: `${colors.cyan('?')} ${message}: `;
|
|
60
|
+
|
|
61
|
+
if (!isInteractive()) {
|
|
62
|
+
return defaultValue ?? '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const rl = createRL();
|
|
67
|
+
|
|
68
|
+
rl.question(promptText, (answer) => {
|
|
69
|
+
rl.close();
|
|
70
|
+
|
|
71
|
+
const value = answer.trim() || defaultValue || '';
|
|
72
|
+
|
|
73
|
+
if (options.validate) {
|
|
74
|
+
const result = options.validate(value);
|
|
75
|
+
if (result !== true) {
|
|
76
|
+
const errorMsg = typeof result === 'string' ? result : 'Invalid value';
|
|
77
|
+
process.stdout.write(`${colors.red('✗')} ${errorMsg}\n`);
|
|
78
|
+
// Re-prompt on validation failure
|
|
79
|
+
prompt(message, options).then(resolve);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resolve(value);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Prompt for confirmation (yes/no)
|
|
91
|
+
*/
|
|
92
|
+
export async function confirm(
|
|
93
|
+
message: string,
|
|
94
|
+
options: ConfirmOptions = {},
|
|
95
|
+
): Promise<boolean> {
|
|
96
|
+
const defaultValue = options.default ?? false;
|
|
97
|
+
const hint = defaultValue ? 'Y/n' : 'y/N';
|
|
98
|
+
|
|
99
|
+
if (!isInteractive()) {
|
|
100
|
+
return defaultValue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const answer = await prompt(`${message} ${colors.dim(`(${hint})`)}`, {
|
|
104
|
+
default: defaultValue ? 'y' : 'n',
|
|
105
|
+
validate: (value) => {
|
|
106
|
+
if (!value) return true;
|
|
107
|
+
return ['y', 'yes', 'n', 'no'].includes(value.toLowerCase()) ||
|
|
108
|
+
'Please enter y or n';
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return ['y', 'yes'].includes(answer.toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Prompt for selection from a list
|
|
117
|
+
*/
|
|
118
|
+
export async function select<T extends string>(
|
|
119
|
+
message: string,
|
|
120
|
+
choices: Array<{ value: T; name?: string; disabled?: boolean }>,
|
|
121
|
+
options: SelectOptions<T> = {},
|
|
122
|
+
): Promise<T> {
|
|
123
|
+
if (!isInteractive()) {
|
|
124
|
+
return options.default ?? choices[0]?.value as T;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pageSize = options.pageSize ?? 10;
|
|
128
|
+
let selectedIndex = choices.findIndex(
|
|
129
|
+
(c) => c.value === options.default && !c.disabled,
|
|
130
|
+
);
|
|
131
|
+
if (selectedIndex === -1) {
|
|
132
|
+
selectedIndex = choices.findIndex((c) => !c.disabled);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
// Hide cursor
|
|
137
|
+
process.stdout.write('\x1b[?25l');
|
|
138
|
+
|
|
139
|
+
const render = () => {
|
|
140
|
+
// Clear previous output
|
|
141
|
+
const lines = Math.min(choices.length, pageSize);
|
|
142
|
+
process.stdout.write(`\x1b[${lines + 1}A\x1b[0J`);
|
|
143
|
+
|
|
144
|
+
// Render prompt
|
|
145
|
+
process.stdout.write(`${colors.cyan('?')} ${message}\n`);
|
|
146
|
+
|
|
147
|
+
// Render choices
|
|
148
|
+
const start = Math.max(0, selectedIndex - pageSize + 1);
|
|
149
|
+
const end = Math.min(choices.length, start + pageSize);
|
|
150
|
+
|
|
151
|
+
for (let i = start; i < end; i++) {
|
|
152
|
+
const choice = choices[i];
|
|
153
|
+
if (!choice) continue;
|
|
154
|
+
|
|
155
|
+
const isSelected = i === selectedIndex;
|
|
156
|
+
const prefix = isSelected ? `${colors.cyan('❯')} ` : ' ';
|
|
157
|
+
const name = choice.name ?? choice.value;
|
|
158
|
+
const text = choice.disabled
|
|
159
|
+
? colors.dim(`${name} (disabled)`)
|
|
160
|
+
: isSelected
|
|
161
|
+
? colors.cyan(name)
|
|
162
|
+
: name;
|
|
163
|
+
|
|
164
|
+
process.stdout.write(`${prefix}${text}\n`);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Initial render
|
|
169
|
+
process.stdout.write(`${colors.cyan('?')} ${message}\n`);
|
|
170
|
+
render();
|
|
171
|
+
|
|
172
|
+
// Handle key input
|
|
173
|
+
const stdin = process.stdin;
|
|
174
|
+
stdin.setRawMode(true);
|
|
175
|
+
stdin.resume();
|
|
176
|
+
stdin.setEncoding('utf8');
|
|
177
|
+
|
|
178
|
+
const cleanup = () => {
|
|
179
|
+
stdin.setRawMode(false);
|
|
180
|
+
stdin.pause();
|
|
181
|
+
stdin.removeListener('data', handler);
|
|
182
|
+
// Show cursor
|
|
183
|
+
process.stdout.write('\x1b[?25h');
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handler = (key: string) => {
|
|
187
|
+
if (key === '\u001b[A' || key === 'k') {
|
|
188
|
+
// Up
|
|
189
|
+
do {
|
|
190
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
191
|
+
} while (choices[selectedIndex]?.disabled);
|
|
192
|
+
render();
|
|
193
|
+
} else if (key === '\u001b[B' || key === 'j') {
|
|
194
|
+
// Down
|
|
195
|
+
do {
|
|
196
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
197
|
+
} while (choices[selectedIndex]?.disabled);
|
|
198
|
+
render();
|
|
199
|
+
} else if (key === '\r' || key === '\n') {
|
|
200
|
+
// Enter
|
|
201
|
+
cleanup();
|
|
202
|
+
const selected = choices[selectedIndex];
|
|
203
|
+
if (selected) {
|
|
204
|
+
process.stdout.write(
|
|
205
|
+
`\x1b[${Math.min(choices.length, pageSize) + 1}A\x1b[0J`,
|
|
206
|
+
);
|
|
207
|
+
process.stdout.write(
|
|
208
|
+
`${colors.cyan('?')} ${message} ${colors.cyan(selected.name ?? selected.value)}\n`,
|
|
209
|
+
);
|
|
210
|
+
resolve(selected.value);
|
|
211
|
+
}
|
|
212
|
+
} else if (key === '\u001b' || key === '\u0003') {
|
|
213
|
+
// Escape or Ctrl+C
|
|
214
|
+
cleanup();
|
|
215
|
+
process.stdout.write('\n');
|
|
216
|
+
process.exit(130);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
stdin.on('data', handler);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Prompt for multiple selections
|
|
226
|
+
*/
|
|
227
|
+
export async function multiSelect<T extends string>(
|
|
228
|
+
message: string,
|
|
229
|
+
choices: Array<{ value: T; name?: string; disabled?: boolean }>,
|
|
230
|
+
options: MultiSelectOptions<T> = {},
|
|
231
|
+
): Promise<T[]> {
|
|
232
|
+
if (!isInteractive()) {
|
|
233
|
+
return options.default ?? [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const pageSize = options.pageSize ?? 10;
|
|
237
|
+
const selected = new Set<T>(options.default ?? []);
|
|
238
|
+
let currentIndex = 0;
|
|
239
|
+
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
// Hide cursor
|
|
242
|
+
process.stdout.write('\x1b[?25l');
|
|
243
|
+
|
|
244
|
+
const render = () => {
|
|
245
|
+
// Clear previous output
|
|
246
|
+
const lines = Math.min(choices.length, pageSize);
|
|
247
|
+
process.stdout.write(`\x1b[${lines + 1}A\x1b[0J`);
|
|
248
|
+
|
|
249
|
+
// Render prompt
|
|
250
|
+
process.stdout.write(`${colors.cyan('?')} ${message}\n`);
|
|
251
|
+
|
|
252
|
+
// Render choices
|
|
253
|
+
const start = Math.max(0, currentIndex - pageSize + 1);
|
|
254
|
+
const end = Math.min(choices.length, start + pageSize);
|
|
255
|
+
|
|
256
|
+
for (let i = start; i < end; i++) {
|
|
257
|
+
const choice = choices[i];
|
|
258
|
+
if (!choice) continue;
|
|
259
|
+
|
|
260
|
+
const isCurrent = i === currentIndex;
|
|
261
|
+
const isSelected = selected.has(choice.value);
|
|
262
|
+
const checkbox = isSelected ? `${colors.green('◉')}` : '○';
|
|
263
|
+
const prefix = isCurrent ? `${colors.cyan('❯')} ` : ' ';
|
|
264
|
+
const name = choice.name ?? choice.value;
|
|
265
|
+
const text = choice.disabled
|
|
266
|
+
? colors.dim(`${name} (disabled)`)
|
|
267
|
+
: isCurrent
|
|
268
|
+
? colors.cyan(name)
|
|
269
|
+
: name;
|
|
270
|
+
|
|
271
|
+
process.stdout.write(`${prefix}${checkbox} ${text}\n`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Initial render
|
|
276
|
+
process.stdout.write(`${colors.cyan('?')} ${message}\n`);
|
|
277
|
+
render();
|
|
278
|
+
|
|
279
|
+
// Handle key input
|
|
280
|
+
const stdin = process.stdin;
|
|
281
|
+
stdin.setRawMode(true);
|
|
282
|
+
stdin.resume();
|
|
283
|
+
stdin.setEncoding('utf8');
|
|
284
|
+
|
|
285
|
+
const cleanup = () => {
|
|
286
|
+
stdin.setRawMode(false);
|
|
287
|
+
stdin.pause();
|
|
288
|
+
stdin.removeListener('data', handler);
|
|
289
|
+
// Show cursor
|
|
290
|
+
process.stdout.write('\x1b[?25h');
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handler = (key: string) => {
|
|
294
|
+
if (key === '\u001b[A' || key === 'k') {
|
|
295
|
+
// Up
|
|
296
|
+
do {
|
|
297
|
+
currentIndex = (currentIndex - 1 + choices.length) % choices.length;
|
|
298
|
+
} while (choices[currentIndex]?.disabled);
|
|
299
|
+
render();
|
|
300
|
+
} else if (key === '\u001b[B' || key === 'j') {
|
|
301
|
+
// Down
|
|
302
|
+
do {
|
|
303
|
+
currentIndex = (currentIndex + 1) % choices.length;
|
|
304
|
+
} while (choices[currentIndex]?.disabled);
|
|
305
|
+
render();
|
|
306
|
+
} else if (key === ' ' || key === 'x') {
|
|
307
|
+
// Toggle selection
|
|
308
|
+
const choice = choices[currentIndex];
|
|
309
|
+
if (choice && !choice.disabled) {
|
|
310
|
+
if (selected.has(choice.value)) {
|
|
311
|
+
if (options.min === undefined || selected.size > options.min) {
|
|
312
|
+
selected.delete(choice.value);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
if (options.max === undefined || selected.size < options.max) {
|
|
316
|
+
selected.add(choice.value);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
render();
|
|
320
|
+
}
|
|
321
|
+
} else if (key === '\r' || key === '\n') {
|
|
322
|
+
// Enter
|
|
323
|
+
cleanup();
|
|
324
|
+
const result = Array.from(selected);
|
|
325
|
+
process.stdout.write(
|
|
326
|
+
`\x1b[${Math.min(choices.length, pageSize) + 1}A\x1b[0J`,
|
|
327
|
+
);
|
|
328
|
+
const names = result
|
|
329
|
+
.map((v) => choices.find((c) => c.value === v)?.name ?? v)
|
|
330
|
+
.join(', ');
|
|
331
|
+
process.stdout.write(
|
|
332
|
+
`${colors.cyan('?')} ${message} ${colors.cyan(names || 'none')}\n`,
|
|
333
|
+
);
|
|
334
|
+
resolve(result);
|
|
335
|
+
} else if (key === '\u001b' || key === '\u0003') {
|
|
336
|
+
// Escape or Ctrl+C
|
|
337
|
+
cleanup();
|
|
338
|
+
process.stdout.write('\n');
|
|
339
|
+
process.exit(130);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
stdin.on('data', handler);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Prompt for a number
|
|
349
|
+
*/
|
|
350
|
+
export async function number(
|
|
351
|
+
message: string,
|
|
352
|
+
options: PromptOptions & { min?: number; max?: number } = {},
|
|
353
|
+
): Promise<number> {
|
|
354
|
+
const value = await prompt(message, {
|
|
355
|
+
...options,
|
|
356
|
+
validate: (v) => {
|
|
357
|
+
if (!v && options.default) return true;
|
|
358
|
+
const num = parseFloat(v);
|
|
359
|
+
if (isNaN(num)) return 'Please enter a valid number';
|
|
360
|
+
if (options.min !== undefined && num < options.min) {
|
|
361
|
+
return `Value must be at least ${options.min}`;
|
|
362
|
+
}
|
|
363
|
+
if (options.max !== undefined && num > options.max) {
|
|
364
|
+
return `Value must be at most ${options.max}`;
|
|
365
|
+
}
|
|
366
|
+
if (options.validate) {
|
|
367
|
+
return options.validate(v);
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return parseFloat(value || options.default || '0');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Prompt for password (hidden input)
|
|
378
|
+
*/
|
|
379
|
+
export async function password(
|
|
380
|
+
message: string,
|
|
381
|
+
options: Omit<PromptOptions, 'default'> = {},
|
|
382
|
+
): Promise<string> {
|
|
383
|
+
if (!isInteractive()) {
|
|
384
|
+
return '';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return new Promise((resolve) => {
|
|
388
|
+
const stdin = process.stdin;
|
|
389
|
+
const stdout = process.stdout;
|
|
390
|
+
|
|
391
|
+
stdout.write(`${colors.cyan('?')} ${message}: `);
|
|
392
|
+
|
|
393
|
+
let value = '';
|
|
394
|
+
|
|
395
|
+
stdin.setRawMode(true);
|
|
396
|
+
stdin.resume();
|
|
397
|
+
stdin.setEncoding('utf8');
|
|
398
|
+
|
|
399
|
+
const cleanup = () => {
|
|
400
|
+
stdin.setRawMode(false);
|
|
401
|
+
stdin.pause();
|
|
402
|
+
stdin.removeListener('data', handler);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const handler = (key: string) => {
|
|
406
|
+
if (key === '\r' || key === '\n') {
|
|
407
|
+
cleanup();
|
|
408
|
+
stdout.write('\n');
|
|
409
|
+
resolve(value);
|
|
410
|
+
} else if (key === '\u0003') {
|
|
411
|
+
cleanup();
|
|
412
|
+
stdout.write('\n');
|
|
413
|
+
process.exit(130);
|
|
414
|
+
} else if (key === '\u007f' || key === '\b') {
|
|
415
|
+
// Backspace
|
|
416
|
+
value = value.slice(0, -1);
|
|
417
|
+
} else if (key[0] !== '\x1b') {
|
|
418
|
+
value += key;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
stdin.on('data', handler);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Spinner for Bueno CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides animated spinners and progress indicators
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { colors, isColorEnabled } from './console';
|
|
8
|
+
|
|
9
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
10
|
+
const SPINNER_INTERVAL = 80;
|
|
11
|
+
|
|
12
|
+
export interface SpinnerOptions {
|
|
13
|
+
text?: string;
|
|
14
|
+
color?: 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Spinner {
|
|
18
|
+
private text: string;
|
|
19
|
+
private color: keyof typeof colors;
|
|
20
|
+
private frameIndex = 0;
|
|
21
|
+
private interval: Timer | null = null;
|
|
22
|
+
private isSpinning = false;
|
|
23
|
+
private stream = process.stdout;
|
|
24
|
+
|
|
25
|
+
constructor(options: SpinnerOptions = {}) {
|
|
26
|
+
this.text = options.text ?? '';
|
|
27
|
+
this.color = options.color ?? 'cyan';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Start the spinner
|
|
32
|
+
*/
|
|
33
|
+
start(text?: string): this {
|
|
34
|
+
if (text) this.text = text;
|
|
35
|
+
if (this.isSpinning) return this;
|
|
36
|
+
|
|
37
|
+
this.isSpinning = true;
|
|
38
|
+
this.frameIndex = 0;
|
|
39
|
+
|
|
40
|
+
// Hide cursor
|
|
41
|
+
this.stream.write('\x1b[?25l');
|
|
42
|
+
|
|
43
|
+
this.interval = setInterval(() => {
|
|
44
|
+
this.render();
|
|
45
|
+
}, SPINNER_INTERVAL);
|
|
46
|
+
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update spinner text
|
|
52
|
+
*/
|
|
53
|
+
update(text: string): this {
|
|
54
|
+
this.text = text;
|
|
55
|
+
if (this.isSpinning) {
|
|
56
|
+
this.render();
|
|
57
|
+
}
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stop the spinner with success
|
|
63
|
+
*/
|
|
64
|
+
success(text?: string): this {
|
|
65
|
+
return this.stop(colors.green('✓'), text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Stop the spinner with error
|
|
70
|
+
*/
|
|
71
|
+
error(text?: string): this {
|
|
72
|
+
return this.stop(colors.red('✗'), text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop the spinner with warning
|
|
77
|
+
*/
|
|
78
|
+
warn(text?: string): this {
|
|
79
|
+
return this.stop(colors.yellow('⚠'), text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Stop the spinner with info
|
|
84
|
+
*/
|
|
85
|
+
info(text?: string): this {
|
|
86
|
+
return this.stop(colors.cyan('ℹ'), text);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stop the spinner
|
|
91
|
+
*/
|
|
92
|
+
stop(symbol?: string, text?: string): this {
|
|
93
|
+
if (!this.isSpinning) return this;
|
|
94
|
+
|
|
95
|
+
this.isSpinning = false;
|
|
96
|
+
|
|
97
|
+
if (this.interval) {
|
|
98
|
+
clearInterval(this.interval);
|
|
99
|
+
this.interval = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Clear the line
|
|
103
|
+
this.stream.write('\r\x1b[K');
|
|
104
|
+
|
|
105
|
+
// Write final message
|
|
106
|
+
const finalText = text ?? this.text;
|
|
107
|
+
if (symbol) {
|
|
108
|
+
this.stream.write(`${symbol} ${finalText}\n`);
|
|
109
|
+
} else {
|
|
110
|
+
this.stream.write(`${finalText}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Show cursor
|
|
114
|
+
this.stream.write('\x1b[?25h');
|
|
115
|
+
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clear the spinner
|
|
121
|
+
*/
|
|
122
|
+
clear(): this {
|
|
123
|
+
if (!this.isSpinning) return this;
|
|
124
|
+
|
|
125
|
+
this.stream.write('\r\x1b[K');
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render current frame
|
|
131
|
+
*/
|
|
132
|
+
private render(): void {
|
|
133
|
+
if (!isColorEnabled()) {
|
|
134
|
+
// Without colors, just show dots
|
|
135
|
+
const dots = '.'.repeat((this.frameIndex % 3) + 1);
|
|
136
|
+
this.stream.write(`\r${this.text}${dots} `);
|
|
137
|
+
this.frameIndex++;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const frame = SPINNER_FRAMES[this.frameIndex % SPINNER_FRAMES.length];
|
|
142
|
+
const coloredFrame = colors[this.color](frame);
|
|
143
|
+
|
|
144
|
+
this.stream.write(`\r${coloredFrame} ${this.text}`);
|
|
145
|
+
this.frameIndex++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create and start a spinner
|
|
151
|
+
*/
|
|
152
|
+
export function spinner(text: string, options?: Omit<SpinnerOptions, 'text'>): Spinner {
|
|
153
|
+
return new Spinner({ text, ...options }).start();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Progress bar
|
|
158
|
+
*/
|
|
159
|
+
export interface ProgressBarOptions {
|
|
160
|
+
total: number;
|
|
161
|
+
width?: number;
|
|
162
|
+
text?: string;
|
|
163
|
+
completeChar?: string;
|
|
164
|
+
incompleteChar?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class ProgressBar {
|
|
168
|
+
private total: number;
|
|
169
|
+
private width: number;
|
|
170
|
+
private text: string;
|
|
171
|
+
private completeChar: string;
|
|
172
|
+
private incompleteChar: string;
|
|
173
|
+
private current = 0;
|
|
174
|
+
private stream = process.stdout;
|
|
175
|
+
|
|
176
|
+
constructor(options: ProgressBarOptions) {
|
|
177
|
+
this.total = options.total;
|
|
178
|
+
this.width = options.width ?? 40;
|
|
179
|
+
this.text = options.text ?? '';
|
|
180
|
+
this.completeChar = options.completeChar ?? '█';
|
|
181
|
+
this.incompleteChar = options.incompleteChar ?? '░';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Start the progress bar
|
|
186
|
+
*/
|
|
187
|
+
start(): this {
|
|
188
|
+
this.current = 0;
|
|
189
|
+
this.render();
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Update progress
|
|
195
|
+
*/
|
|
196
|
+
update(current: number): this {
|
|
197
|
+
this.current = Math.min(current, this.total);
|
|
198
|
+
this.render();
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Increment progress
|
|
204
|
+
*/
|
|
205
|
+
increment(amount = 1): this {
|
|
206
|
+
return this.update(this.current + amount);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Complete the progress bar
|
|
211
|
+
*/
|
|
212
|
+
complete(): this {
|
|
213
|
+
this.current = this.total;
|
|
214
|
+
this.render();
|
|
215
|
+
this.stream.write('\n');
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Render the progress bar
|
|
221
|
+
*/
|
|
222
|
+
private render(): void {
|
|
223
|
+
const percent = this.current / this.total;
|
|
224
|
+
const completeWidth = Math.round(this.width * percent);
|
|
225
|
+
const incompleteWidth = this.width - completeWidth;
|
|
226
|
+
|
|
227
|
+
const complete = this.completeChar.repeat(completeWidth);
|
|
228
|
+
const incomplete = this.incompleteChar.repeat(incompleteWidth);
|
|
229
|
+
|
|
230
|
+
const bar = colors.green(complete) + colors.dim(incomplete);
|
|
231
|
+
const percentText = `${Math.round(percent * 100)}%`.padStart(4);
|
|
232
|
+
|
|
233
|
+
const line = `\r${this.text} [${bar}] ${percentText} ${this.current}/${this.total}`;
|
|
234
|
+
|
|
235
|
+
this.stream.write(`\r\x1b[K${line}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a progress bar
|
|
241
|
+
*/
|
|
242
|
+
export function progressBar(options: ProgressBarOptions): ProgressBar {
|
|
243
|
+
return new ProgressBar(options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Task list with progress
|
|
248
|
+
*/
|
|
249
|
+
export interface TaskOptions {
|
|
250
|
+
text: string;
|
|
251
|
+
task: () => Promise<void>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function runTasks(tasks: TaskOptions[]): Promise<void> {
|
|
255
|
+
for (const task of tasks) {
|
|
256
|
+
const s = spinner(task.text);
|
|
257
|
+
try {
|
|
258
|
+
await task.task();
|
|
259
|
+
s.success();
|
|
260
|
+
} catch (error) {
|
|
261
|
+
s.error();
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|