@agentuity/cli 0.0.72 → 0.0.74

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 (126) hide show
  1. package/bin/cli.ts +19 -5
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +13 -9
  4. package/dist/auth.js.map +1 -1
  5. package/dist/banner.js +1 -1
  6. package/dist/banner.js.map +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +79 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/cmd/ai/prompt/api.d.ts.map +1 -1
  11. package/dist/cmd/ai/prompt/api.js +5 -4
  12. package/dist/cmd/ai/prompt/api.js.map +1 -1
  13. package/dist/cmd/auth/api.d.ts +2 -2
  14. package/dist/cmd/auth/api.d.ts.map +1 -1
  15. package/dist/cmd/auth/api.js +15 -14
  16. package/dist/cmd/auth/api.js.map +1 -1
  17. package/dist/cmd/auth/login.d.ts.map +1 -1
  18. package/dist/cmd/auth/login.js +37 -16
  19. package/dist/cmd/auth/login.js.map +1 -1
  20. package/dist/cmd/auth/ssh/api.d.ts.map +1 -1
  21. package/dist/cmd/auth/ssh/api.js +3 -2
  22. package/dist/cmd/auth/ssh/api.js.map +1 -1
  23. package/dist/cmd/build/ast.d.ts.map +1 -1
  24. package/dist/cmd/build/ast.js +76 -14
  25. package/dist/cmd/build/ast.js.map +1 -1
  26. package/dist/cmd/build/bundler.d.ts +3 -1
  27. package/dist/cmd/build/bundler.d.ts.map +1 -1
  28. package/dist/cmd/build/bundler.js +21 -9
  29. package/dist/cmd/build/bundler.js.map +1 -1
  30. package/dist/cmd/build/format-schema.d.ts +6 -0
  31. package/dist/cmd/build/format-schema.d.ts.map +1 -0
  32. package/dist/cmd/build/format-schema.js +60 -0
  33. package/dist/cmd/build/format-schema.js.map +1 -0
  34. package/dist/cmd/build/index.d.ts.map +1 -1
  35. package/dist/cmd/build/index.js +13 -0
  36. package/dist/cmd/build/index.js.map +1 -1
  37. package/dist/cmd/build/plugin.d.ts.map +1 -1
  38. package/dist/cmd/build/plugin.js +123 -32
  39. package/dist/cmd/build/plugin.js.map +1 -1
  40. package/dist/cmd/build/route-discovery.d.ts +50 -0
  41. package/dist/cmd/build/route-discovery.d.ts.map +1 -0
  42. package/dist/cmd/build/route-discovery.js +143 -0
  43. package/dist/cmd/build/route-discovery.js.map +1 -0
  44. package/dist/cmd/build/route-registry.d.ts.map +1 -1
  45. package/dist/cmd/build/route-registry.js +25 -10
  46. package/dist/cmd/build/route-registry.js.map +1 -1
  47. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  48. package/dist/cmd/cloud/deploy.js +8 -6
  49. package/dist/cmd/cloud/deploy.js.map +1 -1
  50. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  51. package/dist/cmd/cloud/deployment/show.js +34 -10
  52. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  53. package/dist/cmd/dev/agents.d.ts.map +1 -1
  54. package/dist/cmd/dev/agents.js +2 -2
  55. package/dist/cmd/dev/agents.js.map +1 -1
  56. package/dist/cmd/dev/index.d.ts.map +1 -1
  57. package/dist/cmd/dev/index.js +21 -0
  58. package/dist/cmd/dev/index.js.map +1 -1
  59. package/dist/cmd/dev/sync.d.ts.map +1 -1
  60. package/dist/cmd/dev/sync.js +2 -2
  61. package/dist/cmd/dev/sync.js.map +1 -1
  62. package/dist/cmd/project/download.d.ts.map +1 -1
  63. package/dist/cmd/project/download.js +16 -2
  64. package/dist/cmd/project/download.js.map +1 -1
  65. package/dist/cmd/project/list.d.ts.map +1 -1
  66. package/dist/cmd/project/list.js +2 -10
  67. package/dist/cmd/project/list.js.map +1 -1
  68. package/dist/cmd/project/show.d.ts.map +1 -1
  69. package/dist/cmd/project/show.js +8 -7
  70. package/dist/cmd/project/show.js.map +1 -1
  71. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  72. package/dist/cmd/project/template-flow.js +14 -2
  73. package/dist/cmd/project/template-flow.js.map +1 -1
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/config.js +9 -0
  76. package/dist/config.js.map +1 -1
  77. package/dist/index.d.ts +2 -2
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/steps.d.ts +20 -30
  82. package/dist/steps.d.ts.map +1 -1
  83. package/dist/steps.js +339 -184
  84. package/dist/steps.js.map +1 -1
  85. package/dist/tui/box.d.ts.map +1 -1
  86. package/dist/tui/box.js +8 -4
  87. package/dist/tui/box.js.map +1 -1
  88. package/dist/tui/prompt.d.ts.map +1 -1
  89. package/dist/tui/prompt.js +7 -2
  90. package/dist/tui/prompt.js.map +1 -1
  91. package/dist/tui.d.ts +20 -1
  92. package/dist/tui.d.ts.map +1 -1
  93. package/dist/tui.js +90 -18
  94. package/dist/tui.js.map +1 -1
  95. package/dist/types.d.ts +10 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +3 -3
  98. package/src/auth.ts +13 -10
  99. package/src/banner.ts +1 -1
  100. package/src/cli.ts +89 -27
  101. package/src/cmd/ai/prompt/api.ts +5 -4
  102. package/src/cmd/auth/api.ts +20 -22
  103. package/src/cmd/auth/login.ts +36 -17
  104. package/src/cmd/auth/ssh/api.ts +5 -9
  105. package/src/cmd/build/ast.ts +88 -14
  106. package/src/cmd/build/bundler.ts +32 -11
  107. package/src/cmd/build/format-schema.ts +66 -0
  108. package/src/cmd/build/index.ts +14 -0
  109. package/src/cmd/build/plugin.ts +146 -36
  110. package/src/cmd/build/route-discovery.ts +197 -0
  111. package/src/cmd/build/route-registry.ts +26 -10
  112. package/src/cmd/cloud/deploy.ts +19 -6
  113. package/src/cmd/cloud/deployment/show.ts +42 -10
  114. package/src/cmd/dev/agents.ts +2 -10
  115. package/src/cmd/dev/index.ts +25 -0
  116. package/src/cmd/dev/sync.ts +2 -12
  117. package/src/cmd/project/download.ts +16 -2
  118. package/src/cmd/project/list.ts +2 -9
  119. package/src/cmd/project/show.ts +8 -6
  120. package/src/cmd/project/template-flow.ts +21 -2
  121. package/src/config.ts +10 -0
  122. package/src/index.ts +2 -2
  123. package/src/steps.ts +397 -229
  124. package/src/tui/box.ts +8 -4
  125. package/src/tui/prompt.ts +7 -4
  126. package/src/tui.ts +125 -20
package/src/steps.ts CHANGED
@@ -1,66 +1,14 @@
1
1
  /**
2
- * Step progress UI component for showing animated steps with callbacks
2
+ * Steps UI Component v2 - Clean state-driven implementation
3
3
  *
4
- * Displays a checklist where each step animates in place with a spinner,
5
- * then shows success, skipped, or error icon based on callback result.
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 (light/dark adaptive)
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
- let currentColorScheme: ColorScheme = process.env.CI ? 'light' : 'dark';
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 => ({ status: 'skipped', reason });
119
- export const stepError = (message: string, cause?: Error): StepOutcome => ({
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
- * Progress callback function
74
+ * Context passed to step run function
127
75
  */
128
- export type ProgressCallback = (progress: number) => void;
76
+ export interface StepContext {
77
+ progress: (n: number) => void;
78
+ }
129
79
 
130
80
  /**
131
- * Step definition (without progress tracking)
81
+ * Step definition
132
82
  */
133
- export interface SimpleStep {
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
- * Step definition (with progress tracking)
89
+ * Internal step state
141
90
  */
142
- export interface ProgressStep {
143
- type: 'progress';
91
+ type StepStatus = 'pending' | 'running' | 'success' | 'skipped' | 'error';
92
+
93
+ interface StepState {
144
94
  label: string;
145
- run: (progress: ProgressCallback) => Promise<StepOutcome>;
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
- * Step definition (discriminated union)
104
+ * Render a single step line (without output box)
150
105
  */
151
- export type Step = SimpleStep | ProgressStep;
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
- * Internal step state
155
- */
156
- type StepState =
157
- | {
158
- type: 'simple';
159
- label: string;
160
- run: () => Promise<StepOutcome>;
161
- outcome?: StepOutcome;
162
- progress?: number;
163
- }
164
- | {
165
- type: 'progress';
166
- label: string;
167
- run: (progress: ProgressCallback) => Promise<StepOutcome>;
168
- outcome?: StepOutcome;
169
- progress?: number;
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
- * Run a series of steps with animated progress
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
- export async function runSteps(steps: Step[], logLevel?: LogLevel): Promise<void> {
182
- const state: StepState[] = steps.map((s) => {
183
- const stepType = s.type === 'progress' ? 'progress' : 'simple';
184
- return stepType === 'progress'
185
- ? {
186
- type: 'progress' as const,
187
- label: s.label,
188
- run: s.run as (progress: ProgressCallback) => Promise<StepOutcome>,
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
- // Detect if we should use TUI (animated) or plain mode
194
- const useTUI =
195
- process.stdout.isTTY && (!logLevel || ['info', 'warn', 'error'].includes(logLevel));
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
- if (useTUI) {
198
- await runStepsTUI(state);
199
- } else {
200
- await runStepsPlain(state);
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
- * Run steps with animated TUI (original behavior)
188
+ * Global pause state and tracking
217
189
  */
218
- async function runStepsTUI(state: StepState[]): Promise<void> {
219
- // Hide cursor
220
- process.stdout.write('\x1B[?25l');
190
+ let isPaused = false;
191
+ let getTotalLinesFn: (() => number) | null = null;
192
+ let forceRerenderFn: ((skipMove?: boolean) => void) | null = null;
221
193
 
222
- // Track active interval and interrupted state
223
- let activeInterval: ReturnType<typeof setInterval> | null = null;
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 Ctrl+C handler for graceful exit
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
- process.stdout.write(renderSteps(state, -1) + '\n');
367
+ const initialRender = renderAllSteps(state, -1);
368
+ process.stdout.write(initialRender.output + '\n');
369
+ totalLinesFromLastRender = initialRender.totalLines;
240
370
 
241
- for (let stepIndex = 0; stepIndex < state.length; stepIndex++) {
371
+ // Execute steps
372
+ for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
242
373
  if (interrupted) break;
243
374
 
244
- const step = state[stepIndex];
245
- let frameIndex = 0;
246
- let currentFrame = '';
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
- const colorKey = SPINNER_COLORS[frameIndex % SPINNER_COLORS.length];
384
+ if (isPaused) return;
385
+
386
+ const colorKey = SPINNER_COLORS[currentFrameIndex % SPINNER_COLORS.length];
251
387
  const color = getColor(colorKey);
252
- currentFrame = `${color}${COLORS.bold}${FRAMES[frameIndex % FRAMES.length]}${COLORS.reset}`;
388
+ const spinner = `${color}${COLORS.bold}${FRAMES[currentFrameIndex % FRAMES.length]}${COLORS.reset}`;
253
389
 
254
- // Move cursor up to the top of checklist
255
- process.stdout.write(`\x1B[${state.length}A`);
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
- frameIndex++;
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
- // Run the step with progress tracking
262
- const progressCallback: ProgressCallback = (progress: number) => {
263
- step.progress = Math.min(100, Math.max(0, progress));
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
- // Move cursor up and render with current spinner frame
266
- process.stdout.write(`\x1B[${state.length}A`);
267
- process.stdout.write(renderSteps(state, stepIndex, currentFrame) + '\n');
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
- // Use discriminant to determine if step has progress callback
272
- const outcome =
273
- step.type === 'progress' ? await step.run(progressCallback) : await step.run();
274
- step.outcome = outcome;
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
- step.outcome = {
277
- status: 'error',
278
- message: err instanceof Error ? err.message : String(err),
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
- // Clear progress and final render with outcome
289
- step.progress = undefined;
290
- process.stdout.write(`\x1B[${state.length}A`);
291
- process.stdout.write(renderSteps(state, -1) + '\n');
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
- // If error, show error message and exit
294
- if (step.outcome?.status === 'error') {
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: ${step.outcome.message}${COLORS.reset}`);
474
+ console.error(`\n${errorColor}Error: ${stepState.errorMessage}${COLORS.reset}`);
297
475
  if (
298
- step.outcome.cause instanceof ValidationInputError ||
299
- step.outcome.cause instanceof ValidationOutputError
476
+ stepState.errorCause instanceof ValidationInputError ||
477
+ stepState.errorCause instanceof ValidationOutputError
300
478
  ) {
301
- printValidationIssues(step.outcome.cause.issues);
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 again
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 TUI animations)
500
+ * Run steps in plain mode (no animations)
323
501
  */
324
- async function runStepsPlain(state: StepState[]): Promise<void> {
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 state) {
331
- // Run the step (no progress callback for plain mode)
508
+ for (const step of steps) {
509
+ let outcome: StepOutcome;
510
+
332
511
  try {
333
- const outcome = step.type === 'progress' ? await step.run(() => {}) : await step.run();
334
- step.outcome = outcome;
512
+ outcome = await step.run({ progress: () => {} });
335
513
  } catch (err) {
336
- step.outcome = {
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 only
344
- if (step.outcome?.status === 'success') {
521
+ // Print final state
522
+ if (outcome.status === 'success') {
345
523
  console.log(`${greenColor}${ICONS.success}${COLORS.reset} ${step.label}`);
346
- } else if (step.outcome?.status === 'skipped') {
347
- const reason = step.outcome.reason
348
- ? ` ${grayColor}(${step.outcome.reason})${COLORS.reset}`
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
- } else if (step.outcome?.status === 'error') {
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: ${step.outcome.message}${COLORS.reset}`);
554
+ console.error(`\n${errorColor}Error: ${outcome.message}${COLORS.reset}`);
355
555
  if (
356
- step.outcome.cause instanceof ValidationInputError ||
357
- step.outcome.cause instanceof ValidationOutputError
556
+ outcome.cause instanceof ValidationInputError ||
557
+ outcome.cause instanceof ValidationOutputError
358
558
  ) {
359
- printValidationIssues(step.outcome.cause.issues);
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
- * Render a progress indicator
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 renderSteps(steps: StepState[], activeIndex: number, spinner?: string): string {
381
- const grayColor = getColor('gray');
382
- const greenColor = getColor('green');
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
- return lines.join('\n');
574
+ if (useTUI) {
575
+ await runStepsTUI(steps);
576
+ } else {
577
+ await runStepsPlain(steps);
578
+ }
411
579
  }