@agentuity/cli 0.0.6

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 (158) hide show
  1. package/AGENTS.md +139 -0
  2. package/README.md +239 -0
  3. package/bin/cli.ts +71 -0
  4. package/dist/api.d.ts +25 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/auth.d.ts +7 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/banner.d.ts +2 -0
  9. package/dist/banner.d.ts.map +1 -0
  10. package/dist/cli.d.ts +5 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cmd/auth/api.d.ts +9 -0
  13. package/dist/cmd/auth/api.d.ts.map +1 -0
  14. package/dist/cmd/auth/index.d.ts +2 -0
  15. package/dist/cmd/auth/index.d.ts.map +1 -0
  16. package/dist/cmd/auth/login.d.ts +3 -0
  17. package/dist/cmd/auth/login.d.ts.map +1 -0
  18. package/dist/cmd/auth/logout.d.ts +3 -0
  19. package/dist/cmd/auth/logout.d.ts.map +1 -0
  20. package/dist/cmd/bundle/ast.d.ts +2 -0
  21. package/dist/cmd/bundle/ast.d.ts.map +1 -0
  22. package/dist/cmd/bundle/bundler.d.ts +6 -0
  23. package/dist/cmd/bundle/bundler.d.ts.map +1 -0
  24. package/dist/cmd/bundle/file.d.ts +2 -0
  25. package/dist/cmd/bundle/file.d.ts.map +1 -0
  26. package/dist/cmd/bundle/index.d.ts +2 -0
  27. package/dist/cmd/bundle/index.d.ts.map +1 -0
  28. package/dist/cmd/bundle/plugin.d.ts +4 -0
  29. package/dist/cmd/bundle/plugin.d.ts.map +1 -0
  30. package/dist/cmd/dev/index.d.ts +2 -0
  31. package/dist/cmd/dev/index.d.ts.map +1 -0
  32. package/dist/cmd/example/create-user.d.ts +2 -0
  33. package/dist/cmd/example/create-user.d.ts.map +1 -0
  34. package/dist/cmd/example/create.d.ts +2 -0
  35. package/dist/cmd/example/create.d.ts.map +1 -0
  36. package/dist/cmd/example/deploy.d.ts +2 -0
  37. package/dist/cmd/example/deploy.d.ts.map +1 -0
  38. package/dist/cmd/example/index.d.ts +2 -0
  39. package/dist/cmd/example/index.d.ts.map +1 -0
  40. package/dist/cmd/example/list.d.ts +2 -0
  41. package/dist/cmd/example/list.d.ts.map +1 -0
  42. package/dist/cmd/example/run-command.d.ts +2 -0
  43. package/dist/cmd/example/run-command.d.ts.map +1 -0
  44. package/dist/cmd/example/sound.d.ts +3 -0
  45. package/dist/cmd/example/sound.d.ts.map +1 -0
  46. package/dist/cmd/example/spinner.d.ts +2 -0
  47. package/dist/cmd/example/spinner.d.ts.map +1 -0
  48. package/dist/cmd/example/steps.d.ts +2 -0
  49. package/dist/cmd/example/steps.d.ts.map +1 -0
  50. package/dist/cmd/example/version.d.ts +2 -0
  51. package/dist/cmd/example/version.d.ts.map +1 -0
  52. package/dist/cmd/index.d.ts +3 -0
  53. package/dist/cmd/index.d.ts.map +1 -0
  54. package/dist/cmd/profile/create.d.ts +2 -0
  55. package/dist/cmd/profile/create.d.ts.map +1 -0
  56. package/dist/cmd/profile/delete.d.ts +2 -0
  57. package/dist/cmd/profile/delete.d.ts.map +1 -0
  58. package/dist/cmd/profile/index.d.ts +2 -0
  59. package/dist/cmd/profile/index.d.ts.map +1 -0
  60. package/dist/cmd/profile/list.d.ts +3 -0
  61. package/dist/cmd/profile/list.d.ts.map +1 -0
  62. package/dist/cmd/profile/show.d.ts +2 -0
  63. package/dist/cmd/profile/show.d.ts.map +1 -0
  64. package/dist/cmd/profile/use.d.ts +2 -0
  65. package/dist/cmd/profile/use.d.ts.map +1 -0
  66. package/dist/cmd/project/create.d.ts +2 -0
  67. package/dist/cmd/project/create.d.ts.map +1 -0
  68. package/dist/cmd/project/delete.d.ts +2 -0
  69. package/dist/cmd/project/delete.d.ts.map +1 -0
  70. package/dist/cmd/project/index.d.ts +2 -0
  71. package/dist/cmd/project/index.d.ts.map +1 -0
  72. package/dist/cmd/project/list.d.ts +2 -0
  73. package/dist/cmd/project/list.d.ts.map +1 -0
  74. package/dist/cmd/project/show.d.ts +2 -0
  75. package/dist/cmd/project/show.d.ts.map +1 -0
  76. package/dist/cmd/version/index.d.ts +2 -0
  77. package/dist/cmd/version/index.d.ts.map +1 -0
  78. package/dist/command-prefix.d.ts +11 -0
  79. package/dist/command-prefix.d.ts.map +1 -0
  80. package/dist/config.d.ts +16 -0
  81. package/dist/config.d.ts.map +1 -0
  82. package/dist/index.d.ts +18 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/legacy-check.d.ts +6 -0
  85. package/dist/legacy-check.d.ts.map +1 -0
  86. package/dist/logger.d.ts +24 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/runtime.d.ts +3 -0
  89. package/dist/runtime.d.ts.map +1 -0
  90. package/dist/schema-parser.d.ts +24 -0
  91. package/dist/schema-parser.d.ts.map +1 -0
  92. package/dist/sound.d.ts +2 -0
  93. package/dist/sound.d.ts.map +1 -0
  94. package/dist/steps.d.ts +59 -0
  95. package/dist/steps.d.ts.map +1 -0
  96. package/dist/terminal.d.ts +3 -0
  97. package/dist/terminal.d.ts.map +1 -0
  98. package/dist/tui.d.ts +156 -0
  99. package/dist/tui.d.ts.map +1 -0
  100. package/dist/types.d.ts +164 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/version.d.ts +10 -0
  103. package/dist/version.d.ts.map +1 -0
  104. package/package.json +46 -0
  105. package/src/api-errors.md +115 -0
  106. package/src/api.ts +186 -0
  107. package/src/auth.ts +91 -0
  108. package/src/banner.ts +23 -0
  109. package/src/cli.ts +198 -0
  110. package/src/cmd/auth/README.md +95 -0
  111. package/src/cmd/auth/api.ts +71 -0
  112. package/src/cmd/auth/index.ts +9 -0
  113. package/src/cmd/auth/login.ts +76 -0
  114. package/src/cmd/auth/logout.ts +14 -0
  115. package/src/cmd/bundle/ast.ts +228 -0
  116. package/src/cmd/bundle/bundler.ts +88 -0
  117. package/src/cmd/bundle/file.ts +16 -0
  118. package/src/cmd/bundle/index.ts +38 -0
  119. package/src/cmd/bundle/plugin.ts +259 -0
  120. package/src/cmd/dev/index.ts +83 -0
  121. package/src/cmd/example/create-user.ts +38 -0
  122. package/src/cmd/example/create.ts +31 -0
  123. package/src/cmd/example/deploy.ts +36 -0
  124. package/src/cmd/example/index.ts +27 -0
  125. package/src/cmd/example/list.ts +32 -0
  126. package/src/cmd/example/run-command.ts +45 -0
  127. package/src/cmd/example/sound.ts +14 -0
  128. package/src/cmd/example/spinner.ts +44 -0
  129. package/src/cmd/example/steps.ts +66 -0
  130. package/src/cmd/example/version.ts +13 -0
  131. package/src/cmd/index.ts +46 -0
  132. package/src/cmd/profile/README.md +80 -0
  133. package/src/cmd/profile/create.ts +57 -0
  134. package/src/cmd/profile/delete.ts +52 -0
  135. package/src/cmd/profile/index.ts +12 -0
  136. package/src/cmd/profile/list.ts +27 -0
  137. package/src/cmd/profile/show.ts +54 -0
  138. package/src/cmd/profile/use.ts +30 -0
  139. package/src/cmd/project/create.ts +247 -0
  140. package/src/cmd/project/delete.ts +13 -0
  141. package/src/cmd/project/index.ts +11 -0
  142. package/src/cmd/project/list.ts +13 -0
  143. package/src/cmd/project/show.ts +12 -0
  144. package/src/cmd/version/index.ts +16 -0
  145. package/src/command-prefix.ts +43 -0
  146. package/src/config.ts +304 -0
  147. package/src/index.ts +40 -0
  148. package/src/legacy-check.ts +127 -0
  149. package/src/logger.ts +235 -0
  150. package/src/runtime.ts +22 -0
  151. package/src/schema-parser.ts +213 -0
  152. package/src/sound.ts +25 -0
  153. package/src/steps.ts +245 -0
  154. package/src/terminal.ts +151 -0
  155. package/src/tui.md +254 -0
  156. package/src/tui.ts +838 -0
  157. package/src/types.ts +243 -0
  158. package/src/version.ts +29 -0
package/src/tui.ts ADDED
@@ -0,0 +1,838 @@
1
+ /**
2
+ * Terminal UI utilities for formatted, colorized output
3
+ *
4
+ * Provides semantic helpers for console output with automatic icons and colors.
5
+ * Uses Bun's built-in color support and ANSI escape codes.
6
+ */
7
+
8
+ import type { ColorScheme } from './terminal';
9
+
10
+ // Icons
11
+ const ICONS = {
12
+ success: '✓',
13
+ error: '✗',
14
+ warning: '⚠',
15
+ info: 'ℹ',
16
+ arrow: '→',
17
+ bullet: '•',
18
+ } as const;
19
+
20
+ // Color definitions (light/dark adaptive) using Bun.color
21
+ const COLORS = {
22
+ success: {
23
+ light: Bun.color('#008000', 'ansi') || '\x1b[32m', // green
24
+ dark: Bun.color('#00FF00', 'ansi') || '\x1b[92m', // bright green
25
+ },
26
+ error: {
27
+ light: Bun.color('#CC0000', 'ansi') || '\x1b[31m', // red
28
+ dark: Bun.color('#FF5555', 'ansi') || '\x1b[91m', // bright red
29
+ },
30
+ warning: {
31
+ light: Bun.color('#B58900', 'ansi') || '\x1b[33m', // yellow
32
+ dark: Bun.color('#FFFF55', 'ansi') || '\x1b[93m', // bright yellow
33
+ },
34
+ info: {
35
+ light: Bun.color('#008B8B', 'ansi') || '\x1b[36m', // dark cyan
36
+ dark: Bun.color('#55FFFF', 'ansi') || '\x1b[96m', // bright cyan
37
+ },
38
+ muted: {
39
+ light: Bun.color('#808080', 'ansi') || '\x1b[90m', // gray
40
+ dark: Bun.color('#DDDDDD', 'ansi') || '\x1b[37m', // light gray
41
+ },
42
+ bold: {
43
+ light: '\x1b[1m',
44
+ dark: '\x1b[1m',
45
+ },
46
+ link: {
47
+ light: '\x1b[34;4m', // blue underline (need ANSI for underline)
48
+ dark: '\x1b[94;4m', // bright blue underline
49
+ },
50
+ reset: '\x1b[0m',
51
+ } as const;
52
+
53
+ let currentColorScheme: ColorScheme = 'dark';
54
+
55
+ export function setColorScheme(scheme: ColorScheme): void {
56
+ currentColorScheme = scheme;
57
+ }
58
+
59
+ function getColor(colorKey: keyof typeof COLORS): string {
60
+ const color = COLORS[colorKey];
61
+ if (typeof color === 'string') {
62
+ return color;
63
+ }
64
+ return color[currentColorScheme];
65
+ }
66
+
67
+ /**
68
+ * Print a success message with a green checkmark
69
+ */
70
+ export function success(message: string): void {
71
+ const color = getColor('success');
72
+ const reset = COLORS.reset;
73
+ console.log(`${color}${ICONS.success} ${message}${reset}`);
74
+ }
75
+
76
+ /**
77
+ * Print an error message with a red X
78
+ */
79
+ export function error(message: string): void {
80
+ const color = getColor('error');
81
+ const reset = COLORS.reset;
82
+ console.error(`${color}${ICONS.error} ${message}${reset}`);
83
+ }
84
+
85
+ /**
86
+ * Print a warning message with a yellow warning icon
87
+ */
88
+ export function warning(message: string): void {
89
+ const color = getColor('warning');
90
+ const reset = COLORS.reset;
91
+ console.log(`${color}${ICONS.warning} ${message}${reset}`);
92
+ }
93
+
94
+ /**
95
+ * Print an info message with a cyan info icon
96
+ */
97
+ export function info(message: string): void {
98
+ const color = getColor('info');
99
+ const reset = COLORS.reset;
100
+ console.log(`${color}${ICONS.info} ${message}${reset}`);
101
+ }
102
+
103
+ /**
104
+ * Format text in muted/gray color
105
+ */
106
+ export function muted(text: string): string {
107
+ const color = getColor('muted');
108
+ const reset = COLORS.reset;
109
+ return `${color}${text}${reset}`;
110
+ }
111
+
112
+ /**
113
+ * Format text in bold
114
+ */
115
+ export function bold(text: string): string {
116
+ const color = getColor('bold');
117
+ const reset = COLORS.reset;
118
+ return `${color}${text}${reset}`;
119
+ }
120
+
121
+ /**
122
+ * Format text as a link (blue and underlined)
123
+ */
124
+ export function link(url: string): string {
125
+ const color = getColor('link');
126
+ const reset = COLORS.reset;
127
+
128
+ // Check if terminal supports hyperlinks (OSC 8)
129
+ if (supportsHyperlinks()) {
130
+ return `\x1b]8;;${url}\x07${color}${url}${reset}\x1b]8;;\x07`;
131
+ }
132
+
133
+ return `${color}${url}${reset}`;
134
+ }
135
+
136
+ /**
137
+ * Check if terminal supports OSC 8 hyperlinks
138
+ */
139
+ function supportsHyperlinks(): boolean {
140
+ const term = process.env.TERM || '';
141
+ const termProgram = process.env.TERM_PROGRAM || '';
142
+ const wtSession = process.env.WT_SESSION || '';
143
+
144
+ // Known terminal programs that support OSC 8
145
+ return (
146
+ termProgram.includes('iTerm.app') ||
147
+ termProgram.includes('WezTerm') ||
148
+ termProgram.includes('ghostty') ||
149
+ termProgram.includes('Apple_Terminal') ||
150
+ termProgram.includes('Hyper') ||
151
+ term.includes('xterm-kitty') ||
152
+ term.includes('xterm-256color') ||
153
+ wtSession !== '' // Windows Terminal
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Print a bulleted list item
159
+ */
160
+ export function bullet(message: string): void {
161
+ console.log(`${ICONS.bullet} ${message}`);
162
+ }
163
+
164
+ /**
165
+ * Print an arrow item (for showing next steps)
166
+ */
167
+ export function arrow(message: string): void {
168
+ console.log(`${ICONS.arrow} ${message}`);
169
+ }
170
+
171
+ /**
172
+ * Print a blank line
173
+ */
174
+ export function newline(): void {
175
+ console.log('');
176
+ }
177
+
178
+ /**
179
+ * Pad a string to a specific length on the right
180
+ */
181
+ export function padRight(str: string, length: number, pad = ' '): string {
182
+ if (str.length >= length) {
183
+ return str;
184
+ }
185
+ return str + pad.repeat(length - str.length);
186
+ }
187
+
188
+ /**
189
+ * Pad a string to a specific length on the left
190
+ */
191
+ export function padLeft(str: string, length: number, pad = ' '): string {
192
+ if (str.length >= length) {
193
+ return str;
194
+ }
195
+ return pad.repeat(length - str.length) + str;
196
+ }
197
+
198
+ /**
199
+ * Display a formatted banner with title and body content
200
+ * Creates a bordered box around the content
201
+ *
202
+ * Uses Bun.stringWidth() for accurate width calculation with ANSI codes and unicode
203
+ */
204
+ export function banner(title: string, body: string): void {
205
+ const maxWidth = 80;
206
+ const border = {
207
+ topLeft: '╭',
208
+ topRight: '╮',
209
+ bottomLeft: '╰',
210
+ bottomRight: '╯',
211
+ horizontal: '─',
212
+ vertical: '│',
213
+ };
214
+
215
+ // Split body into lines and wrap if needed
216
+ const bodyLines = wrapText(body, maxWidth - 4); // -4 for padding and borders
217
+
218
+ // Calculate width based on content
219
+ const titleWidth = getDisplayWidth(title);
220
+ const maxBodyWidth = Math.max(...bodyLines.map((line) => getDisplayWidth(line)));
221
+ const contentWidth = Math.max(titleWidth, maxBodyWidth);
222
+ const boxWidth = Math.min(contentWidth + 4, maxWidth); // +4 for padding
223
+ const innerWidth = boxWidth - 4;
224
+
225
+ // Colors
226
+ const borderColor = getColor('muted');
227
+ const titleColor = getColor('info');
228
+ const reset = COLORS.reset;
229
+
230
+ // Build banner
231
+ const lines: string[] = [];
232
+
233
+ // Top border
234
+ lines.push(
235
+ `${borderColor}${border.topLeft}${border.horizontal.repeat(boxWidth - 2)}${border.topRight}${reset}`
236
+ );
237
+
238
+ // Empty line
239
+ lines.push(
240
+ `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
241
+ );
242
+
243
+ // Title (centered and bold)
244
+ const titleDisplayWidth = getDisplayWidth(title);
245
+ const titlePadding = Math.max(0, Math.floor((innerWidth - titleDisplayWidth) / 2));
246
+ const titleRightPadding = Math.max(0, innerWidth - titlePadding - titleDisplayWidth);
247
+ const titleLine =
248
+ ' '.repeat(titlePadding) +
249
+ `${titleColor}${bold(title)}${reset}` +
250
+ ' '.repeat(titleRightPadding);
251
+ lines.push(
252
+ `${borderColor}${border.vertical} ${reset}${titleLine}${borderColor} ${border.vertical}${reset}`
253
+ );
254
+
255
+ // Empty line
256
+ lines.push(
257
+ `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
258
+ );
259
+
260
+ // Body lines
261
+ for (const line of bodyLines) {
262
+ const lineWidth = getDisplayWidth(line);
263
+ const padding = Math.max(0, innerWidth - lineWidth);
264
+ lines.push(
265
+ `${borderColor}${border.vertical} ${reset}${line}${' '.repeat(padding)}${borderColor} ${border.vertical}${reset}`
266
+ );
267
+ }
268
+
269
+ // Empty line
270
+ lines.push(
271
+ `${borderColor}${border.vertical}${' '.repeat(boxWidth - 2)}${border.vertical}${reset}`
272
+ );
273
+
274
+ // Bottom border
275
+ lines.push(
276
+ `${borderColor}${border.bottomLeft}${border.horizontal.repeat(boxWidth - 2)}${border.bottomRight}${reset}`
277
+ );
278
+
279
+ // Print the banner
280
+ console.log('\n' + lines.join('\n') + '\n');
281
+ }
282
+
283
+ /**
284
+ * Wait for any key press before continuing
285
+ * Displays a prompt message and waits for user input
286
+ * Exits with code 1 if CTRL+C is pressed
287
+ */
288
+ export async function waitForAnyKey(message = 'Press Enter to continue...'): Promise<void> {
289
+ process.stdout.write(muted(message));
290
+
291
+ // Check if we're in a TTY environment
292
+ if (!process.stdin.isTTY) {
293
+ // Not a TTY (CI/piped), just write newline and exit
294
+ console.log('');
295
+ return Promise.resolve();
296
+ }
297
+
298
+ // Set stdin to raw mode to read a single keypress
299
+ process.stdin.setRawMode(true);
300
+ process.stdin.resume();
301
+ let rawModeSet = true;
302
+
303
+ return new Promise((resolve) => {
304
+ process.stdin.once('data', (data: Buffer) => {
305
+ if (rawModeSet && process.stdin.isTTY) {
306
+ process.stdin.setRawMode(false);
307
+ rawModeSet = false;
308
+ }
309
+ process.stdin.pause();
310
+
311
+ // Check for CTRL+C (character code 3)
312
+ if (data.length === 1 && data[0] === 3) {
313
+ console.log('\n');
314
+ process.exit(1);
315
+ }
316
+
317
+ console.log('');
318
+ resolve();
319
+ });
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Prompts user with a yes/no question
325
+ * Returns true for yes, false for no
326
+ * Exits with code 1 if CTRL+C is pressed
327
+ */
328
+ export async function confirm(message: string, defaultValue = true): Promise<boolean> {
329
+ const suffix = defaultValue ? '[Y/n]' : '[y/N]';
330
+ process.stdout.write(`${message} ${muted(suffix)} `);
331
+
332
+ // Check if we're in a TTY environment
333
+ if (!process.stdin.isTTY) {
334
+ console.log('');
335
+ return defaultValue;
336
+ }
337
+
338
+ // Set stdin to raw mode to read a single keypress
339
+ process.stdin.setRawMode(true);
340
+ process.stdin.resume();
341
+ let rawModeSet = true;
342
+
343
+ return new Promise((resolve) => {
344
+ process.stdin.once('data', (data: Buffer) => {
345
+ if (rawModeSet && process.stdin.isTTY) {
346
+ process.stdin.setRawMode(false);
347
+ rawModeSet = false;
348
+ }
349
+ process.stdin.pause();
350
+
351
+ // Check for CTRL+C (character code 3)
352
+ if (data.length === 1 && data[0] === 3) {
353
+ console.log('\n');
354
+ process.exit(1);
355
+ }
356
+
357
+ const input = data.toString().trim().toLowerCase();
358
+ console.log('');
359
+
360
+ // Enter key (just newline) uses default
361
+ if (input === '') {
362
+ resolve(defaultValue);
363
+ return;
364
+ }
365
+
366
+ // Check first character for y/n
367
+ const char = input.charAt(0);
368
+ if (char === 'y') {
369
+ resolve(true);
370
+ } else if (char === 'n') {
371
+ resolve(false);
372
+ } else {
373
+ // Invalid input, use default
374
+ resolve(defaultValue);
375
+ }
376
+ });
377
+ });
378
+ }
379
+
380
+ /**
381
+ * Copy text to clipboard
382
+ * Returns true if successful, false otherwise
383
+ */
384
+ export async function copyToClipboard(text: string): Promise<boolean> {
385
+ try {
386
+ const platform = process.platform;
387
+
388
+ if (platform === 'darwin') {
389
+ // macOS - use pbcopy
390
+ const proc = Bun.spawn(['pbcopy'], {
391
+ stdin: 'pipe',
392
+ });
393
+ proc.stdin.write(text);
394
+ proc.stdin.end();
395
+ await proc.exited;
396
+ return proc.exitCode === 0;
397
+ } else if (platform === 'win32') {
398
+ // Windows - use clip
399
+ const proc = Bun.spawn(['clip'], {
400
+ stdin: 'pipe',
401
+ });
402
+ proc.stdin.write(text);
403
+ proc.stdin.end();
404
+ await proc.exited;
405
+ return proc.exitCode === 0;
406
+ } else {
407
+ // Linux - try xclip first, then xsel
408
+ try {
409
+ const proc = Bun.spawn(['xclip', '-selection', 'clipboard'], {
410
+ stdin: 'pipe',
411
+ });
412
+ proc.stdin.write(text);
413
+ proc.stdin.end();
414
+ await proc.exited;
415
+ return proc.exitCode === 0;
416
+ } catch {
417
+ // Try xsel as fallback
418
+ const proc = Bun.spawn(['xsel', '--clipboard', '--input'], {
419
+ stdin: 'pipe',
420
+ });
421
+ proc.stdin.write(text);
422
+ proc.stdin.end();
423
+ await proc.exited;
424
+ return proc.exitCode === 0;
425
+ }
426
+ }
427
+ } catch {
428
+ return false;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Get the display width of a string, handling ANSI codes and OSC 8 hyperlinks
434
+ *
435
+ * Note: Bun.stringWidth() counts OSC 8 hyperlink escape sequences in the width,
436
+ * which causes incorrect alignment. We strip OSC 8 codes first, then use Bun.stringWidth()
437
+ * to handle regular ANSI codes and unicode characters correctly.
438
+ */
439
+ function getDisplayWidth(str: string): number {
440
+ // Strip OSC 8 hyperlink sequences: \x1b]8;;URL\x07...\x1b]8;;\x07
441
+ // eslint-disable-next-line no-control-regex
442
+ const withoutOSC8 = str.replace(/\x1b\]8;;[^\x07]*\x07/g, '');
443
+ return Bun.stringWidth(withoutOSC8);
444
+ }
445
+
446
+ /**
447
+ * Wrap text to a maximum width
448
+ * Handles explicit newlines and word wrapping
449
+ */
450
+ function wrapText(text: string, maxWidth: number): string[] {
451
+ const allLines: string[] = [];
452
+
453
+ // First split by explicit newlines
454
+ const paragraphs = text.split('\n');
455
+
456
+ for (const paragraph of paragraphs) {
457
+ // Skip empty paragraphs (they become blank lines)
458
+ if (paragraph.trim() === '') {
459
+ allLines.push('');
460
+ continue;
461
+ }
462
+
463
+ // Wrap each paragraph
464
+ const words = paragraph.split(' ');
465
+ let currentLine = '';
466
+
467
+ for (const word of words) {
468
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
469
+ const testLineWidth = getDisplayWidth(testLine);
470
+
471
+ if (testLineWidth <= maxWidth) {
472
+ currentLine = testLine;
473
+ } else {
474
+ // If current line has content, save it
475
+ if (currentLine) {
476
+ allLines.push(currentLine);
477
+ }
478
+ // If the word itself is longer than maxWidth, just use it as is
479
+ // (better to have a long line than break in the middle)
480
+ currentLine = word;
481
+ }
482
+ }
483
+
484
+ if (currentLine) {
485
+ allLines.push(currentLine);
486
+ }
487
+ }
488
+
489
+ return allLines.length > 0 ? allLines : [''];
490
+ }
491
+
492
+ /**
493
+ * Progress callback for spinner
494
+ */
495
+ export type SpinnerProgressCallback = (progress: number) => void;
496
+
497
+ /**
498
+ * Spinner options (simple without progress)
499
+ */
500
+ export interface SimpleSpinnerOptions<T> {
501
+ type?: 'simple';
502
+ message: string;
503
+ callback: (() => Promise<T>) | Promise<T>;
504
+ }
505
+
506
+ /**
507
+ * Spinner options (with progress tracking)
508
+ */
509
+ export interface ProgressSpinnerOptions<T> {
510
+ type: 'progress';
511
+ message: string;
512
+ callback: (progress: SpinnerProgressCallback) => Promise<T>;
513
+ }
514
+
515
+ /**
516
+ * Spinner options (discriminated union)
517
+ */
518
+ export type SpinnerOptions<T> = SimpleSpinnerOptions<T> | ProgressSpinnerOptions<T>;
519
+
520
+ /**
521
+ * Run a callback with an animated spinner (simple overload)
522
+ *
523
+ * Shows a spinner animation while the callback executes.
524
+ * On success, shows a checkmark. On error, shows an X and re-throws.
525
+ *
526
+ * @param message - The message to display next to the spinner
527
+ * @param callback - Async function or Promise to execute
528
+ */
529
+ export async function spinner<T>(
530
+ message: string,
531
+ callback: (() => Promise<T>) | Promise<T>
532
+ ): Promise<T>;
533
+
534
+ /**
535
+ * Run a callback with an animated spinner (options overload)
536
+ *
537
+ * Shows a spinner animation while the callback executes.
538
+ * On success, shows a checkmark. On error, shows an X and re-throws.
539
+ *
540
+ * @param options - Spinner options with optional progress tracking
541
+ */
542
+ export async function spinner<T>(options: SpinnerOptions<T>): Promise<T>;
543
+
544
+ export async function spinner<T>(
545
+ messageOrOptions: string | SpinnerOptions<T>,
546
+ callback?: (() => Promise<T>) | Promise<T>
547
+ ): Promise<T> {
548
+ // Normalize to options format
549
+ let options: SpinnerOptions<T>;
550
+ if (typeof messageOrOptions === 'string') {
551
+ if (callback === undefined) {
552
+ throw new Error('callback is required when first argument is a string');
553
+ }
554
+ options = { type: 'simple', message: messageOrOptions, callback };
555
+ } else {
556
+ options = messageOrOptions;
557
+ }
558
+
559
+ const message = options.message;
560
+ const frames = ['◐', '◓', '◑', '◒'];
561
+ const spinnerColors = [
562
+ { light: '\x1b[36m', dark: '\x1b[96m' }, // cyan
563
+ { light: '\x1b[34m', dark: '\x1b[94m' }, // blue
564
+ { light: '\x1b[35m', dark: '\x1b[95m' }, // magenta
565
+ { light: '\x1b[36m', dark: '\x1b[96m' }, // cyan
566
+ ];
567
+ const bold = '\x1b[1m';
568
+ const reset = COLORS.reset;
569
+ const cyanColor = { light: '\x1b[36m', dark: '\x1b[96m' }[currentColorScheme];
570
+
571
+ let frameIndex = 0;
572
+ let currentProgress: number | undefined;
573
+
574
+ // Hide cursor
575
+ process.stdout.write('\x1B[?25l');
576
+
577
+ // Start animation
578
+ const interval = setInterval(() => {
579
+ const colorDef = spinnerColors[frameIndex % spinnerColors.length];
580
+ const color = colorDef[currentColorScheme];
581
+ const frame = `${color}${bold}${frames[frameIndex % frames.length]}${reset}`;
582
+
583
+ // Add progress indicator if available
584
+ const progressIndicator =
585
+ currentProgress !== undefined
586
+ ? ` ${cyanColor}${Math.floor(currentProgress)}%${reset}`
587
+ : '';
588
+
589
+ // Clear line and render
590
+ process.stdout.write('\r\x1B[K' + `${frame} ${message}${progressIndicator}`);
591
+ frameIndex++;
592
+ }, 120);
593
+
594
+ // Progress callback
595
+ const progressCallback: SpinnerProgressCallback = (progress: number) => {
596
+ currentProgress = Math.min(100, Math.max(0, progress));
597
+ };
598
+
599
+ try {
600
+ // Execute callback
601
+ const result =
602
+ options.type === 'progress'
603
+ ? await options.callback(progressCallback)
604
+ : typeof options.callback === 'function'
605
+ ? await options.callback()
606
+ : await options.callback;
607
+
608
+ // Clear interval and line
609
+ clearInterval(interval);
610
+ process.stdout.write('\r\x1B[K');
611
+
612
+ // Show success
613
+ const successColor = getColor('success');
614
+ console.log(`${successColor}${ICONS.success} ${message}${reset}`);
615
+
616
+ // Show cursor
617
+ process.stdout.write('\x1B[?25h');
618
+
619
+ return result;
620
+ } catch (err) {
621
+ // Clear interval and line
622
+ clearInterval(interval);
623
+ process.stdout.write('\r\x1B[K');
624
+
625
+ // Show error
626
+ const errorColor = getColor('error');
627
+ console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
628
+
629
+ // Show cursor
630
+ process.stdout.write('\x1B[?25h');
631
+
632
+ throw err;
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Options for running a command with streaming output
638
+ */
639
+ export interface CommandRunnerOptions {
640
+ /**
641
+ * The command to run (displayed in the UI)
642
+ */
643
+ command: string;
644
+ /**
645
+ * The actual command and arguments to execute
646
+ */
647
+ cmd: string[];
648
+ /**
649
+ * Current working directory
650
+ */
651
+ cwd?: string;
652
+ /**
653
+ * Environment variables
654
+ */
655
+ env?: Record<string, string>;
656
+ }
657
+
658
+ /**
659
+ * Run an external command and stream its output with a live UI
660
+ *
661
+ * Displays the command with a colored $ prompt:
662
+ * - Blue while running
663
+ * - Green on successful exit (code 0)
664
+ * - Red on failed exit (code != 0)
665
+ *
666
+ * Shows the last 3 lines of output as it streams.
667
+ */
668
+ export async function runCommand(options: CommandRunnerOptions): Promise<number> {
669
+ const { command, cmd, cwd, env } = options;
670
+ const isTTY = process.stdout.isTTY;
671
+
672
+ // If not a TTY, just run the command normally and log output
673
+ if (!isTTY) {
674
+ const proc = Bun.spawn(cmd, {
675
+ cwd,
676
+ env: { ...process.env, ...env },
677
+ stdout: 'inherit',
678
+ stderr: 'inherit',
679
+ });
680
+ return await proc.exited;
681
+ }
682
+
683
+ // Colors using Bun.color
684
+ const blue =
685
+ currentColorScheme === 'light'
686
+ ? Bun.color('#0000FF', 'ansi') || '\x1b[34m'
687
+ : Bun.color('#5C9CFF', 'ansi') || '\x1b[94m';
688
+ const green = getColor('success');
689
+ const red = getColor('error');
690
+ const cmdColor =
691
+ currentColorScheme === 'light'
692
+ ? '\x1b[1m' + (Bun.color('#00008B', 'ansi') || '\x1b[34m')
693
+ : Bun.color('#FFFFFF', 'ansi') || '\x1b[97m'; // bold dark blue / white
694
+ const mutedColor = Bun.color('#808080', 'ansi') || '\x1b[90m';
695
+ const reset = COLORS.reset;
696
+
697
+ // Get terminal width
698
+ const termWidth = process.stdout.columns || 80;
699
+ const maxCmdWidth = Math.min(40, termWidth);
700
+ const maxLineWidth = Math.min(80, termWidth);
701
+
702
+ // Truncate command if needed
703
+ let displayCmd = command;
704
+ if (getDisplayWidth(displayCmd) > maxCmdWidth) {
705
+ // Simple truncation for now - could be smarter about this
706
+ displayCmd = displayCmd.slice(0, maxCmdWidth - 3) + '...';
707
+ }
708
+
709
+ // Store all output lines, display subset based on context
710
+ const allOutputLines: string[] = [];
711
+ let linesRendered = 0;
712
+
713
+ // Hide cursor
714
+ process.stdout.write('\x1B[?25l');
715
+
716
+ // Render the command and output lines in place
717
+ const renderOutput = (linesToShow: number) => {
718
+ // Move cursor up to start of our output area
719
+ if (linesRendered > 0) {
720
+ process.stdout.write(`\x1b[${linesRendered}A`);
721
+ }
722
+
723
+ // Render command line
724
+ process.stdout.write(`\r\x1b[K${blue}$${reset} ${cmdColor}${displayCmd}${reset}\n`);
725
+
726
+ // Get last N lines to display
727
+ const displayLines = allOutputLines.slice(-linesToShow);
728
+
729
+ // Render output lines
730
+ for (const line of displayLines) {
731
+ // Truncate line if needed
732
+ let displayLine = line;
733
+ if (getDisplayWidth(displayLine) > maxLineWidth) {
734
+ displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
735
+ }
736
+ process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
737
+ }
738
+
739
+ // Update count of lines we've rendered (command + output lines)
740
+ linesRendered = 1 + displayLines.length;
741
+ };
742
+
743
+ // Initial display
744
+ renderOutput(3);
745
+
746
+ try {
747
+ // Spawn the command
748
+ const proc = Bun.spawn(cmd, {
749
+ cwd,
750
+ env: { ...process.env, ...env },
751
+ stdout: 'pipe',
752
+ stderr: 'pipe',
753
+ });
754
+
755
+ // Process output streams
756
+ const processStream = async (stream: ReadableStream<Uint8Array>) => {
757
+ const reader = stream.getReader();
758
+ const decoder = new TextDecoder();
759
+ let buffer = '';
760
+
761
+ try {
762
+ while (true) {
763
+ const { done, value } = await reader.read();
764
+ if (done) break;
765
+
766
+ buffer += decoder.decode(value, { stream: true });
767
+ const lines = buffer.split('\n');
768
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
769
+
770
+ for (const line of lines) {
771
+ if (line.trim()) {
772
+ allOutputLines.push(line);
773
+ renderOutput(3); // Show last 3 lines while streaming
774
+ }
775
+ }
776
+ }
777
+ } finally {
778
+ reader.releaseLock();
779
+ }
780
+ };
781
+
782
+ // Process both stdout and stderr
783
+ await Promise.all([processStream(proc.stdout), processStream(proc.stderr)]);
784
+
785
+ // Wait for process to exit
786
+ const exitCode = await proc.exited;
787
+
788
+ // Determine how many lines to show in final output
789
+ const finalLinesToShow = exitCode === 0 ? 3 : 10;
790
+
791
+ // Move cursor up to redraw final state
792
+ if (linesRendered > 0) {
793
+ process.stdout.write(`\x1b[${linesRendered}A`);
794
+ }
795
+
796
+ // Show final status with appropriate color
797
+ const statusColor = exitCode === 0 ? green : red;
798
+ process.stdout.write(`\r\x1b[K${statusColor}$${reset} ${cmdColor}${displayCmd}${reset}\n`);
799
+
800
+ // Show final output lines
801
+ const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
802
+ for (const line of finalOutputLines) {
803
+ let displayLine = line;
804
+ if (getDisplayWidth(displayLine) > maxLineWidth) {
805
+ displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
806
+ }
807
+ process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
808
+ }
809
+
810
+ return exitCode;
811
+ } catch (err) {
812
+ // Move cursor up to clear our UI
813
+ if (linesRendered > 0) {
814
+ process.stdout.write(`\x1b[${linesRendered}A`);
815
+ // Clear all our lines
816
+ for (let i = 0; i < linesRendered; i++) {
817
+ process.stdout.write('\r\x1b[K\n');
818
+ }
819
+ process.stdout.write(`\x1b[${linesRendered}A`);
820
+ }
821
+
822
+ // Show error status
823
+ process.stdout.write(`\r\x1b[K${red}$${reset} ${cmdColor}${displayCmd}${reset}\n`);
824
+
825
+ // Log the error
826
+ const errorMsg = err instanceof Error ? err.message : String(err);
827
+ console.error(`${red}${ICONS.error} Failed to spawn command: ${errorMsg}${reset}`);
828
+ if (cwd) {
829
+ console.error(`${mutedColor} cwd: ${cwd}${reset}`);
830
+ }
831
+ console.error(`${mutedColor} cmd: ${cmd.join(' ')}${reset}`);
832
+
833
+ return 1; // Return non-zero exit code
834
+ } finally {
835
+ // Always restore cursor visibility
836
+ process.stdout.write('\x1B[?25h');
837
+ }
838
+ }