@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.
- package/AGENTS.md +139 -0
- package/README.md +239 -0
- package/bin/cli.ts +71 -0
- package/dist/api.d.ts +25 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/banner.d.ts +2 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cmd/auth/api.d.ts +9 -0
- package/dist/cmd/auth/api.d.ts.map +1 -0
- package/dist/cmd/auth/index.d.ts +2 -0
- package/dist/cmd/auth/index.d.ts.map +1 -0
- package/dist/cmd/auth/login.d.ts +3 -0
- package/dist/cmd/auth/login.d.ts.map +1 -0
- package/dist/cmd/auth/logout.d.ts +3 -0
- package/dist/cmd/auth/logout.d.ts.map +1 -0
- package/dist/cmd/bundle/ast.d.ts +2 -0
- package/dist/cmd/bundle/ast.d.ts.map +1 -0
- package/dist/cmd/bundle/bundler.d.ts +6 -0
- package/dist/cmd/bundle/bundler.d.ts.map +1 -0
- package/dist/cmd/bundle/file.d.ts +2 -0
- package/dist/cmd/bundle/file.d.ts.map +1 -0
- package/dist/cmd/bundle/index.d.ts +2 -0
- package/dist/cmd/bundle/index.d.ts.map +1 -0
- package/dist/cmd/bundle/plugin.d.ts +4 -0
- package/dist/cmd/bundle/plugin.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts +2 -0
- package/dist/cmd/dev/index.d.ts.map +1 -0
- package/dist/cmd/example/create-user.d.ts +2 -0
- package/dist/cmd/example/create-user.d.ts.map +1 -0
- package/dist/cmd/example/create.d.ts +2 -0
- package/dist/cmd/example/create.d.ts.map +1 -0
- package/dist/cmd/example/deploy.d.ts +2 -0
- package/dist/cmd/example/deploy.d.ts.map +1 -0
- package/dist/cmd/example/index.d.ts +2 -0
- package/dist/cmd/example/index.d.ts.map +1 -0
- package/dist/cmd/example/list.d.ts +2 -0
- package/dist/cmd/example/list.d.ts.map +1 -0
- package/dist/cmd/example/run-command.d.ts +2 -0
- package/dist/cmd/example/run-command.d.ts.map +1 -0
- package/dist/cmd/example/sound.d.ts +3 -0
- package/dist/cmd/example/sound.d.ts.map +1 -0
- package/dist/cmd/example/spinner.d.ts +2 -0
- package/dist/cmd/example/spinner.d.ts.map +1 -0
- package/dist/cmd/example/steps.d.ts +2 -0
- package/dist/cmd/example/steps.d.ts.map +1 -0
- package/dist/cmd/example/version.d.ts +2 -0
- package/dist/cmd/example/version.d.ts.map +1 -0
- package/dist/cmd/index.d.ts +3 -0
- package/dist/cmd/index.d.ts.map +1 -0
- package/dist/cmd/profile/create.d.ts +2 -0
- package/dist/cmd/profile/create.d.ts.map +1 -0
- package/dist/cmd/profile/delete.d.ts +2 -0
- package/dist/cmd/profile/delete.d.ts.map +1 -0
- package/dist/cmd/profile/index.d.ts +2 -0
- package/dist/cmd/profile/index.d.ts.map +1 -0
- package/dist/cmd/profile/list.d.ts +3 -0
- package/dist/cmd/profile/list.d.ts.map +1 -0
- package/dist/cmd/profile/show.d.ts +2 -0
- package/dist/cmd/profile/show.d.ts.map +1 -0
- package/dist/cmd/profile/use.d.ts +2 -0
- package/dist/cmd/profile/use.d.ts.map +1 -0
- package/dist/cmd/project/create.d.ts +2 -0
- package/dist/cmd/project/create.d.ts.map +1 -0
- package/dist/cmd/project/delete.d.ts +2 -0
- package/dist/cmd/project/delete.d.ts.map +1 -0
- package/dist/cmd/project/index.d.ts +2 -0
- package/dist/cmd/project/index.d.ts.map +1 -0
- package/dist/cmd/project/list.d.ts +2 -0
- package/dist/cmd/project/list.d.ts.map +1 -0
- package/dist/cmd/project/show.d.ts +2 -0
- package/dist/cmd/project/show.d.ts.map +1 -0
- package/dist/cmd/version/index.d.ts +2 -0
- package/dist/cmd/version/index.d.ts.map +1 -0
- package/dist/command-prefix.d.ts +11 -0
- package/dist/command-prefix.d.ts.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/legacy-check.d.ts +6 -0
- package/dist/legacy-check.d.ts.map +1 -0
- package/dist/logger.d.ts +24 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/schema-parser.d.ts +24 -0
- package/dist/schema-parser.d.ts.map +1 -0
- package/dist/sound.d.ts +2 -0
- package/dist/sound.d.ts.map +1 -0
- package/dist/steps.d.ts +59 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/terminal.d.ts +3 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/tui.d.ts +156 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/api-errors.md +115 -0
- package/src/api.ts +186 -0
- package/src/auth.ts +91 -0
- package/src/banner.ts +23 -0
- package/src/cli.ts +198 -0
- package/src/cmd/auth/README.md +95 -0
- package/src/cmd/auth/api.ts +71 -0
- package/src/cmd/auth/index.ts +9 -0
- package/src/cmd/auth/login.ts +76 -0
- package/src/cmd/auth/logout.ts +14 -0
- package/src/cmd/bundle/ast.ts +228 -0
- package/src/cmd/bundle/bundler.ts +88 -0
- package/src/cmd/bundle/file.ts +16 -0
- package/src/cmd/bundle/index.ts +38 -0
- package/src/cmd/bundle/plugin.ts +259 -0
- package/src/cmd/dev/index.ts +83 -0
- package/src/cmd/example/create-user.ts +38 -0
- package/src/cmd/example/create.ts +31 -0
- package/src/cmd/example/deploy.ts +36 -0
- package/src/cmd/example/index.ts +27 -0
- package/src/cmd/example/list.ts +32 -0
- package/src/cmd/example/run-command.ts +45 -0
- package/src/cmd/example/sound.ts +14 -0
- package/src/cmd/example/spinner.ts +44 -0
- package/src/cmd/example/steps.ts +66 -0
- package/src/cmd/example/version.ts +13 -0
- package/src/cmd/index.ts +46 -0
- package/src/cmd/profile/README.md +80 -0
- package/src/cmd/profile/create.ts +57 -0
- package/src/cmd/profile/delete.ts +52 -0
- package/src/cmd/profile/index.ts +12 -0
- package/src/cmd/profile/list.ts +27 -0
- package/src/cmd/profile/show.ts +54 -0
- package/src/cmd/profile/use.ts +30 -0
- package/src/cmd/project/create.ts +247 -0
- package/src/cmd/project/delete.ts +13 -0
- package/src/cmd/project/index.ts +11 -0
- package/src/cmd/project/list.ts +13 -0
- package/src/cmd/project/show.ts +12 -0
- package/src/cmd/version/index.ts +16 -0
- package/src/command-prefix.ts +43 -0
- package/src/config.ts +304 -0
- package/src/index.ts +40 -0
- package/src/legacy-check.ts +127 -0
- package/src/logger.ts +235 -0
- package/src/runtime.ts +22 -0
- package/src/schema-parser.ts +213 -0
- package/src/sound.ts +25 -0
- package/src/steps.ts +245 -0
- package/src/terminal.ts +151 -0
- package/src/tui.md +254 -0
- package/src/tui.ts +838 -0
- package/src/types.ts +243 -0
- 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
|
+
}
|