@agentuity/cli 0.0.73 → 0.0.75
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/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +13 -9
- package/dist/auth.js.map +1 -1
- package/dist/banner.js +1 -1
- package/dist/banner.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/cmd/ai/prompt/api.d.ts.map +1 -1
- package/dist/cmd/ai/prompt/api.js +5 -4
- package/dist/cmd/ai/prompt/api.js.map +1 -1
- package/dist/cmd/auth/ssh/api.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/api.js.map +1 -1
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +20 -6
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/bundler.d.ts +3 -1
- package/dist/cmd/build/bundler.d.ts.map +1 -1
- package/dist/cmd/build/bundler.js +19 -9
- package/dist/cmd/build/bundler.js.map +1 -1
- package/dist/cmd/build/plugin.d.ts.map +1 -1
- package/dist/cmd/build/plugin.js +87 -66
- package/dist/cmd/build/plugin.js.map +1 -1
- package/dist/cmd/build/route-discovery.d.ts +50 -0
- package/dist/cmd/build/route-discovery.d.ts.map +1 -0
- package/dist/cmd/build/route-discovery.js +143 -0
- package/dist/cmd/build/route-discovery.js.map +1 -0
- package/dist/cmd/build/route-registry.d.ts.map +1 -1
- package/dist/cmd/build/route-registry.js +25 -10
- package/dist/cmd/build/route-registry.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +8 -6
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/dev/agents.d.ts.map +1 -1
- package/dist/cmd/dev/agents.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +21 -0
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/dev/sync.d.ts.map +1 -1
- package/dist/cmd/dev/sync.js.map +1 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/download.js +16 -2
- package/dist/cmd/project/download.js.map +1 -1
- package/dist/cmd/project/list.d.ts.map +1 -1
- package/dist/cmd/project/list.js +2 -10
- package/dist/cmd/project/list.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/steps.d.ts +20 -30
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +339 -184
- package/dist/steps.js.map +1 -1
- package/dist/tui/box.d.ts.map +1 -1
- package/dist/tui/box.js +8 -4
- package/dist/tui/box.js.map +1 -1
- package/dist/tui/prompt.d.ts.map +1 -1
- package/dist/tui/prompt.js +7 -2
- package/dist/tui/prompt.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +5 -4
- package/dist/tui.js.map +1 -1
- package/package.json +3 -3
- package/src/auth.ts +13 -10
- package/src/banner.ts +1 -1
- package/src/cli.ts +4 -2
- package/src/cmd/ai/prompt/api.ts +5 -4
- package/src/cmd/auth/ssh/api.ts +1 -4
- package/src/cmd/build/ast.ts +21 -6
- package/src/cmd/build/bundler.ts +30 -11
- package/src/cmd/build/plugin.ts +108 -82
- package/src/cmd/build/route-discovery.ts +197 -0
- package/src/cmd/build/route-registry.ts +26 -10
- package/src/cmd/cloud/deploy.ts +19 -6
- package/src/cmd/dev/agents.ts +2 -8
- package/src/cmd/dev/index.ts +25 -0
- package/src/cmd/dev/sync.ts +2 -10
- package/src/cmd/project/download.ts +16 -2
- package/src/cmd/project/list.ts +2 -9
- package/src/index.ts +2 -2
- package/src/steps.ts +397 -229
- package/src/tui/box.ts +8 -4
- package/src/tui/prompt.ts +7 -4
- package/src/tui.ts +6 -4
package/src/steps.ts
CHANGED
|
@@ -1,66 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Steps UI Component v2 - Clean state-driven implementation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Key principle: Render entire step list from state on every update cycle.
|
|
5
|
+
* Track total lines rendered to calculate cursor movement.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ColorScheme } from './terminal';
|
|
9
9
|
import type { LogLevel } from './types';
|
|
10
10
|
import { ValidationInputError, ValidationOutputError, type IssuesType } from '@agentuity/server';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Get the appropriate exit function (Bun.exit or process.exit)
|
|
14
|
-
*/
|
|
15
|
-
function getExitFn(): (code: number) => never {
|
|
16
|
-
const bunExit = (globalThis as { Bun?: { exit?: (code: number) => never } }).Bun?.exit;
|
|
17
|
-
return typeof bunExit === 'function' ? bunExit : process.exit.bind(process);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Install interrupt handlers (SIGINT/SIGTERM + TTY raw mode for Ctrl+C)
|
|
22
|
-
*/
|
|
23
|
-
function installInterruptHandlers(onInterrupt: () => void): () => void {
|
|
24
|
-
const cleanupFns: Array<() => void> = [];
|
|
25
|
-
|
|
26
|
-
const sigHandler = () => onInterrupt();
|
|
27
|
-
process.on('SIGINT', sigHandler);
|
|
28
|
-
process.on('SIGTERM', sigHandler);
|
|
29
|
-
cleanupFns.push(() => {
|
|
30
|
-
process.off('SIGINT', sigHandler);
|
|
31
|
-
process.off('SIGTERM', sigHandler);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// TTY raw mode fallback for Bun/Windows/inconsistent SIGINT delivery
|
|
35
|
-
const stdin = process.stdin as unknown as NodeJS.ReadStream;
|
|
36
|
-
if (stdin && stdin.isTTY) {
|
|
37
|
-
const onData = (buf: Buffer) => {
|
|
38
|
-
// Ctrl+C is ASCII ETX (0x03)
|
|
39
|
-
if (buf.length === 1 && buf[0] === 0x03) onInterrupt();
|
|
40
|
-
};
|
|
41
|
-
try {
|
|
42
|
-
stdin.setRawMode?.(true);
|
|
43
|
-
} catch {
|
|
44
|
-
// ignore if not supported
|
|
45
|
-
}
|
|
46
|
-
stdin.resume?.();
|
|
47
|
-
stdin.on('data', onData);
|
|
48
|
-
cleanupFns.push(() => {
|
|
49
|
-
stdin.off?.('data', onData);
|
|
50
|
-
stdin.pause?.();
|
|
51
|
-
try {
|
|
52
|
-
stdin.setRawMode?.(false);
|
|
53
|
-
} catch {
|
|
54
|
-
// ignore if setRawMode fails
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return () => {
|
|
60
|
-
for (const fn of cleanupFns.splice(0)) fn();
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
12
|
// Spinner frames
|
|
65
13
|
const FRAMES = ['◐', '◓', '◑', '◒'];
|
|
66
14
|
|
|
@@ -72,7 +20,7 @@ const ICONS = {
|
|
|
72
20
|
pending: '☐',
|
|
73
21
|
} as const;
|
|
74
22
|
|
|
75
|
-
// Color definitions
|
|
23
|
+
// Color definitions
|
|
76
24
|
const COLORS = {
|
|
77
25
|
cyan: { light: '\x1b[36m', dark: '\x1b[96m' },
|
|
78
26
|
blue: { light: '\x1b[34m', dark: '\x1b[94m' },
|
|
@@ -86,14 +34,9 @@ const COLORS = {
|
|
|
86
34
|
reset: '\x1b[0m',
|
|
87
35
|
} as const;
|
|
88
36
|
|
|
89
|
-
// Spinner color sequence
|
|
90
37
|
const SPINNER_COLORS = ['cyan', 'blue', 'magenta', 'cyan'] as const;
|
|
91
38
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
export function setStepsColorScheme(scheme: ColorScheme): void {
|
|
95
|
-
currentColorScheme = scheme;
|
|
96
|
-
}
|
|
39
|
+
const currentColorScheme: ColorScheme = process.env.CI ? 'light' : 'dark';
|
|
97
40
|
|
|
98
41
|
function getColor(colorKey: keyof typeof COLORS): string {
|
|
99
42
|
const color = COLORS[colorKey];
|
|
@@ -104,103 +47,132 @@ function getColor(colorKey: keyof typeof COLORS): string {
|
|
|
104
47
|
}
|
|
105
48
|
|
|
106
49
|
/**
|
|
107
|
-
* Step outcome
|
|
50
|
+
* Step outcome returned by step.run()
|
|
108
51
|
*/
|
|
109
52
|
export type StepOutcome =
|
|
110
|
-
| { status: 'success' }
|
|
111
|
-
| { status: 'skipped'; reason?: string }
|
|
112
|
-
| { status: 'error'; message: string; cause?: Error };
|
|
53
|
+
| { status: 'success'; output?: string[] }
|
|
54
|
+
| { status: 'skipped'; reason?: string; output?: string[] }
|
|
55
|
+
| { status: 'error'; message: string; cause?: Error; output?: string[] };
|
|
113
56
|
|
|
114
57
|
/**
|
|
115
58
|
* Helper functions for creating step outcomes
|
|
116
59
|
*/
|
|
117
|
-
export const stepSuccess = (): StepOutcome => ({ status: 'success' });
|
|
118
|
-
export const stepSkipped = (reason?: string): StepOutcome => ({
|
|
119
|
-
|
|
60
|
+
export const stepSuccess = (output?: string[]): StepOutcome => ({ status: 'success', output });
|
|
61
|
+
export const stepSkipped = (reason?: string, output?: string[]): StepOutcome => ({
|
|
62
|
+
status: 'skipped',
|
|
63
|
+
reason,
|
|
64
|
+
output,
|
|
65
|
+
});
|
|
66
|
+
export const stepError = (message: string, cause?: Error, output?: string[]): StepOutcome => ({
|
|
120
67
|
status: 'error',
|
|
121
68
|
message,
|
|
122
69
|
cause,
|
|
70
|
+
output,
|
|
123
71
|
});
|
|
124
72
|
|
|
125
73
|
/**
|
|
126
|
-
*
|
|
74
|
+
* Context passed to step run function
|
|
127
75
|
*/
|
|
128
|
-
export
|
|
76
|
+
export interface StepContext {
|
|
77
|
+
progress: (n: number) => void;
|
|
78
|
+
}
|
|
129
79
|
|
|
130
80
|
/**
|
|
131
|
-
* Step definition
|
|
81
|
+
* Step definition
|
|
132
82
|
*/
|
|
133
|
-
export interface
|
|
134
|
-
type?: 'simple';
|
|
83
|
+
export interface Step {
|
|
135
84
|
label: string;
|
|
136
|
-
run: () => Promise<StepOutcome>;
|
|
85
|
+
run: (ctx: StepContext) => Promise<StepOutcome>;
|
|
137
86
|
}
|
|
138
87
|
|
|
139
88
|
/**
|
|
140
|
-
*
|
|
89
|
+
* Internal step state
|
|
141
90
|
*/
|
|
142
|
-
|
|
143
|
-
|
|
91
|
+
type StepStatus = 'pending' | 'running' | 'success' | 'skipped' | 'error';
|
|
92
|
+
|
|
93
|
+
interface StepState {
|
|
144
94
|
label: string;
|
|
145
|
-
|
|
95
|
+
status: StepStatus;
|
|
96
|
+
progress?: number;
|
|
97
|
+
output?: string[];
|
|
98
|
+
skipReason?: string;
|
|
99
|
+
errorMessage?: string;
|
|
100
|
+
errorCause?: Error;
|
|
146
101
|
}
|
|
147
102
|
|
|
148
103
|
/**
|
|
149
|
-
*
|
|
104
|
+
* Render a single step line (without output box)
|
|
150
105
|
*/
|
|
151
|
-
|
|
106
|
+
function renderStepLine(step: StepState, spinner?: string): string {
|
|
107
|
+
const grayColor = getColor('gray');
|
|
108
|
+
const greenColor = getColor('green');
|
|
109
|
+
const yellowColor = getColor('yellow');
|
|
110
|
+
const redColor = getColor('red');
|
|
111
|
+
const cyanColor = getColor('cyan');
|
|
152
112
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
progress
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
113
|
+
if (step.status === 'success') {
|
|
114
|
+
return `${greenColor}${ICONS.success}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${step.label}${COLORS.reset}`;
|
|
115
|
+
} else if (step.status === 'skipped') {
|
|
116
|
+
const reason = step.skipReason ? ` ${grayColor}(${step.skipReason})${COLORS.reset}` : '';
|
|
117
|
+
return `${yellowColor}${ICONS.skipped}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${step.label}${COLORS.reset}${reason}`;
|
|
118
|
+
} else if (step.status === 'error') {
|
|
119
|
+
return `${redColor}${ICONS.error}${COLORS.reset} ${step.label}`;
|
|
120
|
+
} else if (step.status === 'running' && spinner) {
|
|
121
|
+
const progressIndicator =
|
|
122
|
+
step.progress !== undefined
|
|
123
|
+
? ` ${cyanColor}${Math.floor(step.progress)}%${COLORS.reset}`
|
|
124
|
+
: '';
|
|
125
|
+
return `${spinner} ${step.label}${progressIndicator}`;
|
|
126
|
+
} else {
|
|
127
|
+
return `${grayColor}${ICONS.pending}${COLORS.reset} ${step.label}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
171
130
|
|
|
172
131
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* Each step runs its callback while showing a spinner animation.
|
|
176
|
-
* Steps can complete with success, skipped, or error status.
|
|
177
|
-
* Exits with code 1 if any step errors.
|
|
178
|
-
*
|
|
179
|
-
* When there's no TTY or log level is debug/trace, uses plain output instead of TUI.
|
|
132
|
+
* Render all steps from state, including output boxes
|
|
133
|
+
* Returns the rendered output and total line count
|
|
180
134
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
: { type: 'simple' as const, label: s.label, run: s.run as () => Promise<StepOutcome> };
|
|
191
|
-
});
|
|
135
|
+
function renderAllSteps(
|
|
136
|
+
state: StepState[],
|
|
137
|
+
runningStepIndex: number,
|
|
138
|
+
spinner?: string
|
|
139
|
+
): { output: string; totalLines: number } {
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
let totalLines = 0;
|
|
142
|
+
const grayColor = getColor('gray');
|
|
192
143
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
144
|
+
for (let i = 0; i < state.length; i++) {
|
|
145
|
+
const step = state[i];
|
|
146
|
+
const isRunning = i === runningStepIndex;
|
|
147
|
+
const stepSpinner = isRunning && spinner ? spinner : undefined;
|
|
196
148
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
149
|
+
// Render step line
|
|
150
|
+
lines.push(renderStepLine(step, stepSpinner));
|
|
151
|
+
totalLines++;
|
|
152
|
+
|
|
153
|
+
// Render output box if present
|
|
154
|
+
if (step.output && step.output.length > 0) {
|
|
155
|
+
lines.push(`${grayColor}╭─ Output${COLORS.reset}`);
|
|
156
|
+
totalLines++;
|
|
157
|
+
|
|
158
|
+
for (const line of step.output) {
|
|
159
|
+
lines.push(`${grayColor}│${COLORS.reset} ${line}`);
|
|
160
|
+
totalLines++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
lines.push(`${grayColor}╰─${COLORS.reset}`);
|
|
164
|
+
totalLines++;
|
|
165
|
+
|
|
166
|
+
// Don't add blank line here - the '\n' we append during write creates separation
|
|
167
|
+
}
|
|
201
168
|
}
|
|
169
|
+
|
|
170
|
+
return { output: lines.join('\n'), totalLines };
|
|
202
171
|
}
|
|
203
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Print validation issues (for ValidationInputError/ValidationOutputError)
|
|
175
|
+
*/
|
|
204
176
|
function printValidationIssues(issues?: IssuesType) {
|
|
205
177
|
const errorColor = getColor('red');
|
|
206
178
|
console.error(`${errorColor}Validation details:${COLORS.reset}`);
|
|
@@ -213,17 +185,147 @@ function printValidationIssues(issues?: IssuesType) {
|
|
|
213
185
|
}
|
|
214
186
|
|
|
215
187
|
/**
|
|
216
|
-
*
|
|
188
|
+
* Global pause state and tracking
|
|
217
189
|
*/
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
190
|
+
let isPaused = false;
|
|
191
|
+
let getTotalLinesFn: (() => number) | null = null;
|
|
192
|
+
let forceRerenderFn: ((skipMove?: boolean) => void) | null = null;
|
|
221
193
|
|
|
222
|
-
|
|
223
|
-
|
|
194
|
+
export function isStepUIPaused(): boolean {
|
|
195
|
+
return isPaused;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Internal function to set pause capability
|
|
200
|
+
*/
|
|
201
|
+
function enablePauseResume(
|
|
202
|
+
getTotalLines: () => number,
|
|
203
|
+
forceRerender: (skipMove?: boolean) => void
|
|
204
|
+
): void {
|
|
205
|
+
getTotalLinesFn = getTotalLines;
|
|
206
|
+
forceRerenderFn = forceRerender;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Pause step rendering for interactive input
|
|
211
|
+
* Returns resume function
|
|
212
|
+
*/
|
|
213
|
+
export function pauseStepUI(): () => void {
|
|
214
|
+
if (!process.stdout.isTTY || !getTotalLinesFn) {
|
|
215
|
+
return () => {}; // No-op if not TTY or not in step context
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
isPaused = true;
|
|
219
|
+
|
|
220
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
221
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
222
|
+
|
|
223
|
+
// Intercept writes during pause (unused but prevents issues with interactive prompts)
|
|
224
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
225
|
+
process.stdout.write = ((chunk: any, ..._args: any[]) => {
|
|
226
|
+
return originalStdoutWrite(chunk);
|
|
227
|
+
}) as typeof process.stdout.write;
|
|
228
|
+
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
process.stderr.write = ((chunk: any, ..._args: any[]) => {
|
|
231
|
+
return originalStderrWrite(chunk);
|
|
232
|
+
}) as typeof process.stderr.write;
|
|
233
|
+
|
|
234
|
+
// Show cursor and add newline for separation
|
|
235
|
+
process.stdout.write('\x1B[?25h');
|
|
236
|
+
process.stdout.write('\n');
|
|
237
|
+
|
|
238
|
+
// Return resume function
|
|
239
|
+
return () => {
|
|
240
|
+
isPaused = false;
|
|
241
|
+
|
|
242
|
+
// Restore original write functions
|
|
243
|
+
process.stdout.write = originalStdoutWrite;
|
|
244
|
+
process.stderr.write = originalStderrWrite;
|
|
245
|
+
|
|
246
|
+
// Restore cursor to saved position (where steps began)
|
|
247
|
+
process.stdout.write('\x1B[u'); // Restore cursor position
|
|
248
|
+
process.stdout.write('\x1B[0J'); // Clear from saved position to end of screen
|
|
249
|
+
process.stdout.write('\x1B[?25l'); // Hide cursor
|
|
250
|
+
|
|
251
|
+
// Force immediate re-render (cursor already at step start)
|
|
252
|
+
if (forceRerenderFn) {
|
|
253
|
+
forceRerenderFn(true);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get exit function (Bun.exit or process.exit)
|
|
260
|
+
*/
|
|
261
|
+
function getExitFn(): (code: number) => never {
|
|
262
|
+
const bunExit = (globalThis as { Bun?: { exit?: (code: number) => never } }).Bun?.exit;
|
|
263
|
+
return typeof bunExit === 'function' ? bunExit : process.exit.bind(process);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Install interrupt handlers (SIGINT/SIGTERM + raw mode)
|
|
268
|
+
*/
|
|
269
|
+
function installInterruptHandlers(onInterrupt: () => void): () => void {
|
|
270
|
+
const cleanupFns: Array<() => void> = [];
|
|
271
|
+
|
|
272
|
+
const sigHandler = () => onInterrupt();
|
|
273
|
+
process.on('SIGINT', sigHandler);
|
|
274
|
+
process.on('SIGTERM', sigHandler);
|
|
275
|
+
cleanupFns.push(() => {
|
|
276
|
+
process.off('SIGINT', sigHandler);
|
|
277
|
+
process.off('SIGTERM', sigHandler);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// TTY raw mode fallback
|
|
281
|
+
const stdin = process.stdin as unknown as NodeJS.ReadStream;
|
|
282
|
+
if (stdin && stdin.isTTY) {
|
|
283
|
+
const onData = (buf: Buffer) => {
|
|
284
|
+
if (buf.length === 1 && buf[0] === 0x03) onInterrupt();
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
stdin.setRawMode?.(true);
|
|
288
|
+
} catch {
|
|
289
|
+
// Ignore errors
|
|
290
|
+
}
|
|
291
|
+
stdin.resume?.();
|
|
292
|
+
stdin.on('data', onData);
|
|
293
|
+
cleanupFns.push(() => {
|
|
294
|
+
stdin.off?.('data', onData);
|
|
295
|
+
stdin.pause?.();
|
|
296
|
+
try {
|
|
297
|
+
stdin.setRawMode?.(false);
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore errors
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return () => {
|
|
305
|
+
for (const fn of cleanupFns.splice(0)) fn();
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Run steps with TUI (animated mode)
|
|
311
|
+
*/
|
|
312
|
+
async function runStepsTUI(steps: Step[]): Promise<void> {
|
|
313
|
+
// Initialize state
|
|
314
|
+
const state: StepState[] = steps.map((s) => ({
|
|
315
|
+
label: s.label,
|
|
316
|
+
status: 'pending' as const,
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
let totalLinesFromLastRender = 0;
|
|
224
320
|
let interrupted = false;
|
|
321
|
+
let activeInterval: ReturnType<typeof setInterval> | null = null;
|
|
322
|
+
let currentStepIndex = -1;
|
|
323
|
+
let currentFrameIndex = 0;
|
|
324
|
+
|
|
325
|
+
// Hide cursor
|
|
326
|
+
process.stdout.write('\x1B[?25l');
|
|
225
327
|
|
|
226
|
-
// Set up
|
|
328
|
+
// Set up interrupt handler
|
|
227
329
|
const exit = getExitFn();
|
|
228
330
|
const onInterrupt = () => {
|
|
229
331
|
if (interrupted) return;
|
|
@@ -234,71 +336,147 @@ async function runStepsTUI(state: StepState[]): Promise<void> {
|
|
|
234
336
|
};
|
|
235
337
|
const restoreInterrupts = installInterruptHandlers(onInterrupt);
|
|
236
338
|
|
|
339
|
+
// Force re-render function
|
|
340
|
+
const forceRerender = (skipMove = false) => {
|
|
341
|
+
if (currentStepIndex < 0 || currentStepIndex >= state.length) return;
|
|
342
|
+
|
|
343
|
+
const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length];
|
|
344
|
+
const color = getColor(colorKey);
|
|
345
|
+
const spinner = `${color}${COLORS.bold}${FRAMES[currentFrameIndex % FRAMES.length]}${COLORS.reset}`;
|
|
346
|
+
const rendered = renderAllSteps(state, currentStepIndex, spinner);
|
|
347
|
+
|
|
348
|
+
// Optionally move up, then to column 0
|
|
349
|
+
if (!skipMove && totalLinesFromLastRender > 0) {
|
|
350
|
+
process.stdout.write(`\x1B[${totalLinesFromLastRender}A`);
|
|
351
|
+
process.stdout.write('\x1B[0G');
|
|
352
|
+
}
|
|
353
|
+
process.stdout.write('\x1B[0J');
|
|
354
|
+
process.stdout.write(rendered.output + '\n');
|
|
355
|
+
|
|
356
|
+
totalLinesFromLastRender = rendered.totalLines;
|
|
357
|
+
};
|
|
358
|
+
|
|
237
359
|
try {
|
|
360
|
+
// Enable pause/resume capability
|
|
361
|
+
enablePauseResume(() => totalLinesFromLastRender, forceRerender);
|
|
362
|
+
|
|
363
|
+
// Save cursor position BEFORE rendering steps
|
|
364
|
+
process.stdout.write('\x1B[s');
|
|
365
|
+
|
|
238
366
|
// Initial render
|
|
239
|
-
|
|
367
|
+
const initialRender = renderAllSteps(state, -1);
|
|
368
|
+
process.stdout.write(initialRender.output + '\n');
|
|
369
|
+
totalLinesFromLastRender = initialRender.totalLines;
|
|
240
370
|
|
|
241
|
-
|
|
371
|
+
// Execute steps
|
|
372
|
+
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
242
373
|
if (interrupted) break;
|
|
243
374
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
375
|
+
currentStepIndex = stepIndex;
|
|
376
|
+
currentFrameIndex = 0;
|
|
377
|
+
|
|
378
|
+
const step = steps[stepIndex];
|
|
379
|
+
const stepState = state[stepIndex];
|
|
380
|
+
stepState.status = 'running';
|
|
247
381
|
|
|
248
382
|
// Start spinner animation
|
|
249
383
|
activeInterval = setInterval(() => {
|
|
250
|
-
|
|
384
|
+
if (isPaused) return;
|
|
385
|
+
|
|
386
|
+
const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length];
|
|
251
387
|
const color = getColor(colorKey);
|
|
252
|
-
|
|
388
|
+
const spinner = `${color}${COLORS.bold}${FRAMES[currentFrameIndex % FRAMES.length]}${COLORS.reset}`;
|
|
253
389
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
process.stdout.write(renderSteps(state, stepIndex, currentFrame) + '\n');
|
|
390
|
+
// Render all steps from state
|
|
391
|
+
const rendered = renderAllSteps(state, currentStepIndex, spinner);
|
|
257
392
|
|
|
258
|
-
|
|
393
|
+
// Move to start, clear, render
|
|
394
|
+
if (totalLinesFromLastRender > 0) {
|
|
395
|
+
process.stdout.write(`\x1B[${totalLinesFromLastRender}A`); // Move up
|
|
396
|
+
process.stdout.write('\x1B[0G'); // Move to column 0 (using absolute positioning)
|
|
397
|
+
}
|
|
398
|
+
process.stdout.write('\x1B[0J'); // Clear from cursor to end
|
|
399
|
+
process.stdout.write(rendered.output + '\n');
|
|
400
|
+
|
|
401
|
+
totalLinesFromLastRender = rendered.totalLines;
|
|
402
|
+
currentFrameIndex++;
|
|
259
403
|
}, 120);
|
|
260
404
|
|
|
261
|
-
//
|
|
262
|
-
const progressCallback
|
|
263
|
-
|
|
405
|
+
// Progress callback
|
|
406
|
+
const progressCallback = (progress: number) => {
|
|
407
|
+
if (isPaused) return;
|
|
408
|
+
|
|
409
|
+
stepState.progress = Math.min(100, Math.max(0, progress));
|
|
264
410
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
411
|
+
// Render all steps from state
|
|
412
|
+
const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length];
|
|
413
|
+
const color = getColor(colorKey);
|
|
414
|
+
const spinner = `${color}${COLORS.bold}${FRAMES[currentFrameIndex % FRAMES.length]}${COLORS.reset}`;
|
|
415
|
+
const rendered = renderAllSteps(state, currentStepIndex, spinner);
|
|
416
|
+
|
|
417
|
+
// Move to start, clear, render
|
|
418
|
+
if (totalLinesFromLastRender > 0) {
|
|
419
|
+
process.stdout.write(`\x1B[${totalLinesFromLastRender}A`);
|
|
420
|
+
process.stdout.write('\x1B[0G');
|
|
421
|
+
}
|
|
422
|
+
process.stdout.write('\x1B[0J');
|
|
423
|
+
process.stdout.write(rendered.output + '\n');
|
|
424
|
+
|
|
425
|
+
totalLinesFromLastRender = rendered.totalLines;
|
|
268
426
|
};
|
|
269
427
|
|
|
428
|
+
// Run the step
|
|
270
429
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
430
|
+
const outcome = await step.run({ progress: progressCallback });
|
|
431
|
+
|
|
432
|
+
// Update state from outcome
|
|
433
|
+
if (outcome.status === 'success') {
|
|
434
|
+
stepState.status = 'success';
|
|
435
|
+
stepState.output = outcome.output;
|
|
436
|
+
} else if (outcome.status === 'skipped') {
|
|
437
|
+
stepState.status = 'skipped';
|
|
438
|
+
stepState.skipReason = outcome.reason;
|
|
439
|
+
stepState.output = outcome.output;
|
|
440
|
+
} else {
|
|
441
|
+
stepState.status = 'error';
|
|
442
|
+
stepState.errorMessage = outcome.message;
|
|
443
|
+
stepState.errorCause = outcome.cause;
|
|
444
|
+
stepState.output = outcome.output;
|
|
445
|
+
}
|
|
275
446
|
} catch (err) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
cause: err instanceof Error ? err : undefined,
|
|
280
|
-
};
|
|
447
|
+
stepState.status = 'error';
|
|
448
|
+
stepState.errorMessage = err instanceof Error ? err.message : String(err);
|
|
449
|
+
stepState.errorCause = err instanceof Error ? err : undefined;
|
|
281
450
|
}
|
|
282
451
|
|
|
452
|
+
// Stop spinner
|
|
283
453
|
if (activeInterval) {
|
|
284
454
|
clearInterval(activeInterval);
|
|
285
455
|
activeInterval = null;
|
|
286
456
|
}
|
|
287
457
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
458
|
+
// Final render with outcome
|
|
459
|
+
stepState.progress = undefined;
|
|
460
|
+
const finalRender = renderAllSteps(state, -1);
|
|
461
|
+
|
|
462
|
+
if (totalLinesFromLastRender > 0) {
|
|
463
|
+
process.stdout.write(`\x1B[${totalLinesFromLastRender}A`);
|
|
464
|
+
process.stdout.write('\x1B[0G');
|
|
465
|
+
}
|
|
466
|
+
process.stdout.write('\x1B[0J');
|
|
467
|
+
process.stdout.write(finalRender.output + '\n');
|
|
292
468
|
|
|
293
|
-
|
|
294
|
-
|
|
469
|
+
totalLinesFromLastRender = finalRender.totalLines;
|
|
470
|
+
|
|
471
|
+
// Handle errors
|
|
472
|
+
if (stepState.status === 'error') {
|
|
295
473
|
const errorColor = getColor('red');
|
|
296
|
-
console.error(`\n${errorColor}Error: ${
|
|
474
|
+
console.error(`\n${errorColor}Error: ${stepState.errorMessage}${COLORS.reset}`);
|
|
297
475
|
if (
|
|
298
|
-
|
|
299
|
-
|
|
476
|
+
stepState.errorCause instanceof ValidationInputError ||
|
|
477
|
+
stepState.errorCause instanceof ValidationOutputError
|
|
300
478
|
) {
|
|
301
|
-
printValidationIssues(
|
|
479
|
+
printValidationIssues(stepState.errorCause.issues);
|
|
302
480
|
}
|
|
303
481
|
console.error('');
|
|
304
482
|
process.stdout.write('\x1B[?25h'); // Show cursor
|
|
@@ -306,57 +484,79 @@ async function runStepsTUI(state: StepState[]): Promise<void> {
|
|
|
306
484
|
}
|
|
307
485
|
}
|
|
308
486
|
|
|
309
|
-
// Show cursor
|
|
487
|
+
// Show cursor
|
|
310
488
|
process.stdout.write('\x1B[?25h');
|
|
311
489
|
} catch (err) {
|
|
312
|
-
// Ensure cursor is shown even if something goes wrong
|
|
313
490
|
process.stdout.write('\x1B[?25h');
|
|
314
491
|
throw err;
|
|
315
492
|
} finally {
|
|
316
|
-
// Remove signal/TTY handlers
|
|
317
493
|
restoreInterrupts();
|
|
494
|
+
getTotalLinesFn = null; // Clear pause capability
|
|
495
|
+
forceRerenderFn = null;
|
|
318
496
|
}
|
|
319
497
|
}
|
|
320
498
|
|
|
321
499
|
/**
|
|
322
|
-
* Run steps in plain mode (no
|
|
500
|
+
* Run steps in plain mode (no animations)
|
|
323
501
|
*/
|
|
324
|
-
async function runStepsPlain(
|
|
502
|
+
async function runStepsPlain(steps: Step[]): Promise<void> {
|
|
325
503
|
const grayColor = getColor('gray');
|
|
326
504
|
const greenColor = getColor('green');
|
|
327
505
|
const yellowColor = getColor('yellow');
|
|
328
506
|
const redColor = getColor('red');
|
|
329
507
|
|
|
330
|
-
for (const step of
|
|
331
|
-
|
|
508
|
+
for (const step of steps) {
|
|
509
|
+
let outcome: StepOutcome;
|
|
510
|
+
|
|
332
511
|
try {
|
|
333
|
-
|
|
334
|
-
step.outcome = outcome;
|
|
512
|
+
outcome = await step.run({ progress: () => {} });
|
|
335
513
|
} catch (err) {
|
|
336
|
-
|
|
514
|
+
outcome = {
|
|
337
515
|
status: 'error',
|
|
338
516
|
message: err instanceof Error ? err.message : String(err),
|
|
339
517
|
cause: err instanceof Error ? err : undefined,
|
|
340
518
|
};
|
|
341
519
|
}
|
|
342
520
|
|
|
343
|
-
// Print final state
|
|
344
|
-
if (
|
|
521
|
+
// Print final state
|
|
522
|
+
if (outcome.status === 'success') {
|
|
345
523
|
console.log(`${greenColor}${ICONS.success}${COLORS.reset} ${step.label}`);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
524
|
+
if (outcome.output && outcome.output.length > 0) {
|
|
525
|
+
console.log(`${grayColor}╭─ Output${COLORS.reset}`);
|
|
526
|
+
for (const line of outcome.output) {
|
|
527
|
+
console.log(`${grayColor}│${COLORS.reset} ${line}`);
|
|
528
|
+
}
|
|
529
|
+
console.log(`${grayColor}╰─${COLORS.reset}`);
|
|
530
|
+
console.log('');
|
|
531
|
+
}
|
|
532
|
+
} else if (outcome.status === 'skipped') {
|
|
533
|
+
const reason = outcome.reason ? ` ${grayColor}(${outcome.reason})${COLORS.reset}` : '';
|
|
350
534
|
console.log(`${yellowColor}${ICONS.skipped}${COLORS.reset} ${step.label}${reason}`);
|
|
351
|
-
|
|
535
|
+
if (outcome.output && outcome.output.length > 0) {
|
|
536
|
+
console.log(`${grayColor}╭─ Output${COLORS.reset}`);
|
|
537
|
+
for (const line of outcome.output) {
|
|
538
|
+
console.log(`${grayColor}│${COLORS.reset} ${line}`);
|
|
539
|
+
}
|
|
540
|
+
console.log(`${grayColor}╰─${COLORS.reset}`);
|
|
541
|
+
console.log('');
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
352
544
|
console.log(`${redColor}${ICONS.error}${COLORS.reset} ${step.label}`);
|
|
545
|
+
if (outcome.output && outcome.output.length > 0) {
|
|
546
|
+
console.log(`${grayColor}╭─ Output${COLORS.reset}`);
|
|
547
|
+
for (const line of outcome.output) {
|
|
548
|
+
console.log(`${grayColor}│${COLORS.reset} ${line}`);
|
|
549
|
+
}
|
|
550
|
+
console.log(`${grayColor}╰─${COLORS.reset}`);
|
|
551
|
+
console.log('');
|
|
552
|
+
}
|
|
353
553
|
const errorColor = getColor('red');
|
|
354
|
-
console.error(`\n${errorColor}Error: ${
|
|
554
|
+
console.error(`\n${errorColor}Error: ${outcome.message}${COLORS.reset}`);
|
|
355
555
|
if (
|
|
356
|
-
|
|
357
|
-
|
|
556
|
+
outcome.cause instanceof ValidationInputError ||
|
|
557
|
+
outcome.cause instanceof ValidationOutputError
|
|
358
558
|
) {
|
|
359
|
-
printValidationIssues(
|
|
559
|
+
printValidationIssues(outcome.cause.issues);
|
|
360
560
|
}
|
|
361
561
|
console.error('');
|
|
362
562
|
process.exit(1);
|
|
@@ -365,47 +565,15 @@ async function runStepsPlain(state: StepState[]): Promise<void> {
|
|
|
365
565
|
}
|
|
366
566
|
|
|
367
567
|
/**
|
|
368
|
-
*
|
|
369
|
-
*/
|
|
370
|
-
function renderProgress(progress: number): string {
|
|
371
|
-
const cyanColor = getColor('cyan');
|
|
372
|
-
|
|
373
|
-
const percentage = `${Math.floor(progress)}%`;
|
|
374
|
-
return ` ${cyanColor}${percentage}${COLORS.reset}`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Render all steps as a multiline string
|
|
568
|
+
* Run a series of steps with animated progress
|
|
379
569
|
*/
|
|
380
|
-
function
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
const yellowColor = getColor('yellow');
|
|
384
|
-
const redColor = getColor('red');
|
|
385
|
-
|
|
386
|
-
const lines: string[] = [];
|
|
387
|
-
|
|
388
|
-
steps.forEach((s, i) => {
|
|
389
|
-
// Don't show progress indicator for steps with outcomes (success/skipped/error)
|
|
390
|
-
if (s.outcome?.status === 'success') {
|
|
391
|
-
lines.push(
|
|
392
|
-
`${greenColor}${ICONS.success}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${s.label}${COLORS.reset}`
|
|
393
|
-
);
|
|
394
|
-
} else if (s.outcome?.status === 'skipped') {
|
|
395
|
-
const reason = s.outcome.reason ? ` ${grayColor}(${s.outcome.reason})${COLORS.reset}` : '';
|
|
396
|
-
lines.push(
|
|
397
|
-
`${yellowColor}${ICONS.skipped}${COLORS.reset} ${grayColor}${COLORS.strikethrough}${s.label}${COLORS.reset}${reason}`
|
|
398
|
-
);
|
|
399
|
-
} else if (s.outcome?.status === 'error') {
|
|
400
|
-
lines.push(`${redColor}${ICONS.error}${COLORS.reset} ${s.label}`);
|
|
401
|
-
} else if (i === activeIndex && spinner) {
|
|
402
|
-
// Only show progress for active step with spinner
|
|
403
|
-
const progressIndicator = s.progress !== undefined ? renderProgress(s.progress) : '';
|
|
404
|
-
lines.push(`${spinner} ${s.label}${progressIndicator}`);
|
|
405
|
-
} else {
|
|
406
|
-
lines.push(`${grayColor}${ICONS.pending}${COLORS.reset} ${s.label}`);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
570
|
+
export async function runSteps(steps: Step[], logLevel?: LogLevel): Promise<void> {
|
|
571
|
+
const useTUI =
|
|
572
|
+
process.stdout.isTTY && (!logLevel || ['info', 'warn', 'error'].includes(logLevel));
|
|
409
573
|
|
|
410
|
-
|
|
574
|
+
if (useTUI) {
|
|
575
|
+
await runStepsTUI(steps);
|
|
576
|
+
} else {
|
|
577
|
+
await runStepsPlain(steps);
|
|
578
|
+
}
|
|
411
579
|
}
|