@agentuity/cli 0.0.71 → 0.0.73

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 (132) hide show
  1. package/bin/cli.ts +19 -5
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +77 -19
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cmd/auth/api.d.ts +2 -2
  6. package/dist/cmd/auth/api.d.ts.map +1 -1
  7. package/dist/cmd/auth/api.js +15 -14
  8. package/dist/cmd/auth/api.js.map +1 -1
  9. package/dist/cmd/auth/login.d.ts.map +1 -1
  10. package/dist/cmd/auth/login.js +37 -16
  11. package/dist/cmd/auth/login.js.map +1 -1
  12. package/dist/cmd/auth/ssh/api.d.ts.map +1 -1
  13. package/dist/cmd/auth/ssh/api.js +3 -2
  14. package/dist/cmd/auth/ssh/api.js.map +1 -1
  15. package/dist/cmd/build/ast.d.ts.map +1 -1
  16. package/dist/cmd/build/ast.js +56 -8
  17. package/dist/cmd/build/ast.js.map +1 -1
  18. package/dist/cmd/build/bundler.d.ts +1 -2
  19. package/dist/cmd/build/bundler.d.ts.map +1 -1
  20. package/dist/cmd/build/bundler.js +30 -8
  21. package/dist/cmd/build/bundler.js.map +1 -1
  22. package/dist/cmd/build/format-schema.d.ts +6 -0
  23. package/dist/cmd/build/format-schema.d.ts.map +1 -0
  24. package/dist/cmd/build/format-schema.js +60 -0
  25. package/dist/cmd/build/format-schema.js.map +1 -0
  26. package/dist/cmd/build/index.d.ts.map +1 -1
  27. package/dist/cmd/build/index.js +13 -0
  28. package/dist/cmd/build/index.js.map +1 -1
  29. package/dist/cmd/build/plugin.d.ts.map +1 -1
  30. package/dist/cmd/build/plugin.js +72 -2
  31. package/dist/cmd/build/plugin.js.map +1 -1
  32. package/dist/cmd/cloud/db/create.js +2 -2
  33. package/dist/cmd/cloud/db/create.js.map +1 -1
  34. package/dist/cmd/cloud/db/delete.js +2 -2
  35. package/dist/cmd/cloud/db/delete.js.map +1 -1
  36. package/dist/cmd/cloud/db/get.js +2 -2
  37. package/dist/cmd/cloud/db/get.js.map +1 -1
  38. package/dist/cmd/cloud/db/list.js +2 -2
  39. package/dist/cmd/cloud/db/list.js.map +1 -1
  40. package/dist/cmd/cloud/db/logs.js +2 -2
  41. package/dist/cmd/cloud/db/logs.js.map +1 -1
  42. package/dist/cmd/cloud/db/sql.js +2 -2
  43. package/dist/cmd/cloud/db/sql.js.map +1 -1
  44. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  45. package/dist/cmd/cloud/deploy.js +163 -24
  46. package/dist/cmd/cloud/deploy.js.map +1 -1
  47. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  48. package/dist/cmd/cloud/deployment/show.js +34 -10
  49. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  50. package/dist/cmd/cloud/session/get.js +2 -2
  51. package/dist/cmd/cloud/session/get.js.map +1 -1
  52. package/dist/cmd/cloud/session/list.js +2 -2
  53. package/dist/cmd/cloud/session/list.js.map +1 -1
  54. package/dist/cmd/cloud/storage/create.js +2 -2
  55. package/dist/cmd/cloud/storage/create.js.map +1 -1
  56. package/dist/cmd/cloud/storage/delete.js +2 -2
  57. package/dist/cmd/cloud/storage/delete.js.map +1 -1
  58. package/dist/cmd/cloud/storage/download.js +2 -2
  59. package/dist/cmd/cloud/storage/download.js.map +1 -1
  60. package/dist/cmd/cloud/storage/get.js +2 -2
  61. package/dist/cmd/cloud/storage/get.js.map +1 -1
  62. package/dist/cmd/cloud/storage/list.js +2 -2
  63. package/dist/cmd/cloud/storage/list.js.map +1 -1
  64. package/dist/cmd/cloud/storage/upload.js +2 -2
  65. package/dist/cmd/cloud/storage/upload.js.map +1 -1
  66. package/dist/cmd/cloud/thread/delete.js +2 -2
  67. package/dist/cmd/cloud/thread/delete.js.map +1 -1
  68. package/dist/cmd/cloud/thread/get.js +2 -2
  69. package/dist/cmd/cloud/thread/get.js.map +1 -1
  70. package/dist/cmd/cloud/thread/list.js +2 -2
  71. package/dist/cmd/cloud/thread/list.js.map +1 -1
  72. package/dist/cmd/dev/agents.d.ts.map +1 -1
  73. package/dist/cmd/dev/agents.js +2 -2
  74. package/dist/cmd/dev/agents.js.map +1 -1
  75. package/dist/cmd/dev/sync.d.ts.map +1 -1
  76. package/dist/cmd/dev/sync.js +2 -2
  77. package/dist/cmd/dev/sync.js.map +1 -1
  78. package/dist/cmd/project/show.d.ts.map +1 -1
  79. package/dist/cmd/project/show.js +8 -7
  80. package/dist/cmd/project/show.js.map +1 -1
  81. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  82. package/dist/cmd/project/template-flow.js +14 -2
  83. package/dist/cmd/project/template-flow.js.map +1 -1
  84. package/dist/config.d.ts +2 -1
  85. package/dist/config.d.ts.map +1 -1
  86. package/dist/config.js +17 -1
  87. package/dist/config.js.map +1 -1
  88. package/dist/output.d.ts.map +1 -1
  89. package/dist/output.js +5 -1
  90. package/dist/output.js.map +1 -1
  91. package/dist/tui.d.ts +46 -1
  92. package/dist/tui.d.ts.map +1 -1
  93. package/dist/tui.js +217 -39
  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/cli.ts +85 -25
  99. package/src/cmd/auth/api.ts +20 -26
  100. package/src/cmd/auth/login.ts +36 -17
  101. package/src/cmd/auth/ssh/api.ts +5 -6
  102. package/src/cmd/build/ast.ts +67 -8
  103. package/src/cmd/build/bundler.ts +37 -9
  104. package/src/cmd/build/format-schema.ts +66 -0
  105. package/src/cmd/build/index.ts +14 -0
  106. package/src/cmd/build/plugin.ts +86 -2
  107. package/src/cmd/cloud/db/create.ts +2 -2
  108. package/src/cmd/cloud/db/delete.ts +2 -2
  109. package/src/cmd/cloud/db/get.ts +2 -2
  110. package/src/cmd/cloud/db/list.ts +2 -2
  111. package/src/cmd/cloud/db/logs.ts +2 -2
  112. package/src/cmd/cloud/db/sql.ts +2 -2
  113. package/src/cmd/cloud/deploy.ts +187 -24
  114. package/src/cmd/cloud/deployment/show.ts +42 -10
  115. package/src/cmd/cloud/session/get.ts +2 -2
  116. package/src/cmd/cloud/session/list.ts +2 -2
  117. package/src/cmd/cloud/storage/create.ts +2 -2
  118. package/src/cmd/cloud/storage/delete.ts +2 -2
  119. package/src/cmd/cloud/storage/download.ts +2 -2
  120. package/src/cmd/cloud/storage/get.ts +2 -2
  121. package/src/cmd/cloud/storage/list.ts +2 -2
  122. package/src/cmd/cloud/storage/upload.ts +2 -2
  123. package/src/cmd/cloud/thread/delete.ts +2 -2
  124. package/src/cmd/cloud/thread/get.ts +2 -2
  125. package/src/cmd/cloud/thread/list.ts +2 -2
  126. package/src/cmd/dev/agents.ts +2 -4
  127. package/src/cmd/dev/sync.ts +6 -8
  128. package/src/cmd/project/show.ts +8 -6
  129. package/src/cmd/project/template-flow.ts +21 -2
  130. package/src/config.ts +19 -6
  131. package/src/output.ts +7 -1
  132. package/src/tui.ts +302 -41
package/src/config.ts CHANGED
@@ -51,6 +51,16 @@ export async function saveProfile(path: string): Promise<void> {
51
51
  }
52
52
 
53
53
  export async function getProfile(): Promise<string> {
54
+ // Check environment variable first
55
+ if (process.env.AGENTUITY_PROFILE) {
56
+ const profileName = process.env.AGENTUITY_PROFILE;
57
+ const envProfilePath = join(getDefaultConfigDir(), `${profileName}.yaml`);
58
+ const envFile = Bun.file(envProfilePath);
59
+ if (await envFile.exists()) {
60
+ return envProfilePath;
61
+ }
62
+ }
63
+
54
64
  const profilePath = getProfilePath();
55
65
  const defaultConfigPath = getDefaultConfigPath();
56
66
 
@@ -727,12 +737,7 @@ export async function loadProjectSDKKey(
727
737
  logger.trace(`[SDK_KEY] AGENTUITY_SDK_KEY not found in any file`);
728
738
  }
729
739
 
730
- export function getCatalystAPIClient(
731
- config: Config | null,
732
- logger: Logger,
733
- auth: AuthData,
734
- region: string
735
- ) {
740
+ export function getCatalystAPIClient(logger: Logger, auth: AuthData, region: string) {
736
741
  const serviceUrls = getServiceUrls(region);
737
742
  const catalystUrl = serviceUrls.catalyst;
738
743
  return new ServerAPIClient(catalystUrl, logger, auth.apiKey);
@@ -745,3 +750,11 @@ export function getIONHost(config: Config | null) {
745
750
  const url = new URL(config?.overrides?.ion_url ?? 'https://ion.agentuity.cloud');
746
751
  return url.hostname;
747
752
  }
753
+
754
+ export function getStreamURL(region: string, config: Config | null) {
755
+ if (config?.name === 'local') {
756
+ return 'https://streams.agentuity.io';
757
+ }
758
+ const serviceUrls = getServiceUrls(region);
759
+ return serviceUrls.stream;
760
+ }
package/src/output.ts CHANGED
@@ -39,7 +39,13 @@ export function isQuietMode(options: GlobalOptions): boolean {
39
39
  * Check if progress indicators should be disabled
40
40
  */
41
41
  export function shouldDisableProgress(options: GlobalOptions): boolean {
42
- return options.noProgress === true || options.json === true || options.quiet === true;
42
+ return (
43
+ options.noProgress === true ||
44
+ options.json === true ||
45
+ options.quiet === true ||
46
+ options.logLevel === 'debug' ||
47
+ options.logLevel === 'trace'
48
+ );
43
49
  }
44
50
 
45
51
  /**
package/src/tui.ts CHANGED
@@ -341,6 +341,85 @@ function getDisplayWidth(str: string): number {
341
341
  return Bun.stringWidth(withoutOSC8);
342
342
  }
343
343
 
344
+ /**
345
+ * Strip all ANSI escape sequences from a string
346
+ */
347
+ function stripAnsi(str: string): string {
348
+ // eslint-disable-next-line no-control-regex
349
+ return str.replace(/\u001b\[[0-9;]*m/g, '').replace(/\u001b\]8;;[^\u0007]*\u0007/g, '');
350
+ }
351
+
352
+ /**
353
+ * Truncate a string to a maximum display width, handling ANSI codes and Unicode correctly
354
+ * Preserves ANSI escape sequences and doesn't break multi-byte characters or grapheme clusters
355
+ */
356
+ function truncateToWidth(str: string, maxWidth: number, ellipsis = '...'): string {
357
+ const totalWidth = getDisplayWidth(str);
358
+ if (totalWidth <= maxWidth) {
359
+ return str;
360
+ }
361
+
362
+ // Strip ANSI to get visible text
363
+ const visible = stripAnsi(str);
364
+
365
+ // Use Intl.Segmenter for grapheme-aware iteration
366
+ const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
367
+ const segments = Array.from(segmenter.segment(visible));
368
+
369
+ // Find the cutoff point by accumulating display width
370
+ let currentWidth = 0;
371
+ let cutIndex = 0;
372
+ const targetWidth = maxWidth - ellipsis.length;
373
+
374
+ for (let i = 0; i < segments.length; i++) {
375
+ const segment = segments[i].segment;
376
+ const segmentWidth = Bun.stringWidth(segment);
377
+
378
+ if (currentWidth + segmentWidth > targetWidth) {
379
+ break;
380
+ }
381
+
382
+ currentWidth += segmentWidth;
383
+ cutIndex = segments[i].index + segment.length;
384
+ }
385
+
386
+ // Now reconstruct with ANSI codes preserved
387
+ // Walk through original string and copy characters + ANSI codes until we hit cutIndex in visible content
388
+ let result = '';
389
+ let visibleIndex = 0;
390
+ let i = 0;
391
+
392
+ while (i < str.length && visibleIndex < cutIndex) {
393
+ // Check for ANSI escape sequence
394
+ if (str[i] === '\u001b') {
395
+ // Copy entire ANSI sequence
396
+ // eslint-disable-next-line no-control-regex
397
+ const match = str.slice(i).match(/^\u001b\[[0-9;]*m/);
398
+ if (match) {
399
+ result += match[0];
400
+ i += match[0].length;
401
+ continue;
402
+ }
403
+
404
+ // Check for OSC 8 hyperlink
405
+ // eslint-disable-next-line no-control-regex
406
+ const oscMatch = str.slice(i).match(/^\u001b\]8;;[^\u0007]*\u0007/);
407
+ if (oscMatch) {
408
+ result += oscMatch[0];
409
+ i += oscMatch[0].length;
410
+ continue;
411
+ }
412
+ }
413
+
414
+ // Copy visible character
415
+ result += str[i];
416
+ visibleIndex++;
417
+ i++;
418
+ }
419
+
420
+ return result + ellipsis;
421
+ }
422
+
344
423
  /**
345
424
  * Pad a string to a specific length on the right
346
425
  */
@@ -796,6 +875,11 @@ function wrapText(text: string, maxWidth: number): string[] {
796
875
  */
797
876
  export type SpinnerProgressCallback = (progress: number) => void;
798
877
 
878
+ /**
879
+ * Log callback for spinner
880
+ */
881
+ export type SpinnerLogCallback = (message: string) => void;
882
+
799
883
  /**
800
884
  * Spinner options (simple without progress)
801
885
  */
@@ -824,10 +908,53 @@ export interface ProgressSpinnerOptions<T> {
824
908
  clearOnSuccess?: boolean;
825
909
  }
826
910
 
911
+ /**
912
+ * Spinner options (with logger streaming)
913
+ */
914
+ export interface LoggerSpinnerOptions<T> {
915
+ type: 'logger';
916
+ message: string;
917
+ callback: (log: SpinnerLogCallback) => Promise<T>;
918
+ /**
919
+ * If true, clear the spinner output on success (no icon, no message)
920
+ * Defaults to false
921
+ */
922
+ clearOnSuccess?: boolean;
923
+ /**
924
+ * Maximum number of log lines to show while running
925
+ * If < 0, shows all lines. Defaults to 3.
926
+ */
927
+ maxLines?: number;
928
+ }
929
+
930
+ /**
931
+ * Spinner options (with countdown timer)
932
+ */
933
+ export interface CountdownSpinnerOptions<T> {
934
+ type: 'countdown';
935
+ message: string;
936
+ timeoutMs: number;
937
+ callback: () => Promise<T>;
938
+ /**
939
+ * If true, clear the spinner output on success (no icon, no message)
940
+ * Defaults to false
941
+ */
942
+ clearOnSuccess?: boolean;
943
+ /**
944
+ * Optional callback to handle Enter key press
945
+ * Can be used to open a URL in the browser
946
+ */
947
+ onEnterPress?: () => void;
948
+ }
949
+
827
950
  /**
828
951
  * Spinner options (discriminated union)
829
952
  */
830
- export type SpinnerOptions<T> = SimpleSpinnerOptions<T> | ProgressSpinnerOptions<T>;
953
+ export type SpinnerOptions<T> =
954
+ | SimpleSpinnerOptions<T>
955
+ | ProgressSpinnerOptions<T>
956
+ | LoggerSpinnerOptions<T>
957
+ | CountdownSpinnerOptions<T>;
831
958
 
832
959
  /**
833
960
  * Run a callback with an animated spinner (simple overload)
@@ -882,9 +1009,16 @@ export async function spinner<T>(
882
1009
  const result =
883
1010
  options.type === 'progress'
884
1011
  ? await options.callback(() => {})
885
- : typeof options.callback === 'function'
886
- ? await options.callback()
887
- : await options.callback;
1012
+ : options.type === 'logger'
1013
+ ? await options.callback((logMessage: string) => {
1014
+ // In non-TTY mode, just write logs directly to stdout
1015
+ process.stdout.write(logMessage + '\n');
1016
+ })
1017
+ : options.type === 'countdown'
1018
+ ? await options.callback()
1019
+ : typeof options.callback === 'function'
1020
+ ? await options.callback()
1021
+ : await options.callback;
888
1022
 
889
1023
  // If clearOnSuccess is true, don't show success message
890
1024
  // Also skip success message in JSON mode
@@ -914,47 +1048,168 @@ export async function spinner<T>(
914
1048
 
915
1049
  let frameIndex = 0;
916
1050
  let currentProgress: number | undefined;
1051
+ let remainingTime: number | undefined;
1052
+ const logLines: string[] = [];
1053
+ const maxLines = options.type === 'logger' ? (options.maxLines ?? 3) : 0;
1054
+ const mutedColor = getColor('muted');
1055
+ let linesRendered = 0;
917
1056
 
918
- // Save cursor position and hide cursor
919
- process.stderr.write('\x1B[s\x1B[?25l');
1057
+ // Get terminal width for truncation
1058
+ const termWidth = process.stderr.columns || 80;
1059
+ const maxLineWidth = Math.min(80, termWidth);
1060
+
1061
+ // Function to render spinner with optional log lines
1062
+ const renderSpinner = () => {
1063
+ // Move cursor up to start of our output area if we've rendered before
1064
+ if (linesRendered > 0) {
1065
+ process.stderr.write(`\x1b[${linesRendered}A`);
1066
+ }
920
1067
 
921
- // Start animation
922
- const interval = setInterval(() => {
923
1068
  const colorDef = spinnerColors[frameIndex % spinnerColors.length];
924
1069
  const color = colorDef[currentColorScheme];
925
1070
  const frame = `${color}${bold}${frames[frameIndex % frames.length]}${reset}`;
926
1071
 
927
- // Add progress indicator if available
928
- const progressIndicator =
929
- currentProgress !== undefined
930
- ? ` ${cyanColor}${Math.floor(currentProgress)}%${reset}`
931
- : '';
1072
+ // Add progress indicator or countdown timer if available
1073
+ let indicator = '';
1074
+ if (currentProgress !== undefined) {
1075
+ indicator = ` ${cyanColor}${Math.floor(currentProgress)}%${reset}`;
1076
+ } else if (remainingTime !== undefined) {
1077
+ const minutes = Math.floor(remainingTime / 60);
1078
+ const seconds = Math.floor(remainingTime % 60);
1079
+ const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
1080
+ indicator = ` ${mutedColor}(${timeStr} remaining)${reset}`;
1081
+ }
1082
+
1083
+ // Render spinner line
1084
+ process.stderr.write(`\r\x1b[K${frame} ${message}${indicator}\n`);
1085
+
1086
+ // Render log lines if in logger mode
1087
+ if (options.type === 'logger') {
1088
+ const displayLines = maxLines < 0 ? logLines : logLines.slice(-maxLines);
1089
+ for (const line of displayLines) {
1090
+ const displayLine =
1091
+ getDisplayWidth(line) > maxLineWidth ? truncateToWidth(line, maxLineWidth) : line;
1092
+ process.stderr.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
1093
+ }
1094
+ linesRendered = 1 + displayLines.length;
1095
+ } else {
1096
+ linesRendered = 1;
1097
+ }
932
1098
 
933
- // Clear line and render
934
- process.stderr.write('\r\x1B[K' + `${frame} ${message}${progressIndicator}`);
935
1099
  frameIndex++;
936
- }, 120);
1100
+ };
1101
+
1102
+ // Save cursor position and hide cursor
1103
+ process.stderr.write('\x1B[s\x1B[?25l');
1104
+
1105
+ // Initial render
1106
+ renderSpinner();
1107
+
1108
+ // Start animation
1109
+ const interval = setInterval(renderSpinner, 120);
937
1110
 
938
1111
  // Progress callback
939
1112
  const progressCallback: SpinnerProgressCallback = (progress: number) => {
940
1113
  currentProgress = Math.min(100, Math.max(0, progress));
941
1114
  };
942
1115
 
1116
+ // Log callback
1117
+ const logCallback: SpinnerLogCallback = (logMessage: string) => {
1118
+ logLines.push(logMessage);
1119
+ };
1120
+
1121
+ // Countdown interval tracking
1122
+ let countdownInterval: NodeJS.Timeout | undefined;
1123
+ let keypressListener: ((chunk: Buffer) => void) | undefined;
1124
+
1125
+ // Helper to clean up all resources
1126
+ const cleanup = () => {
1127
+ if (countdownInterval) {
1128
+ clearInterval(countdownInterval);
1129
+ }
1130
+ if (keypressListener) {
1131
+ process.stdin.off('data', keypressListener);
1132
+ if (process.stdin.isTTY) {
1133
+ process.stdin.setRawMode(false);
1134
+ process.stdin.pause();
1135
+ }
1136
+ }
1137
+ process.off('SIGINT', cleanupAndExit);
1138
+ };
1139
+
1140
+ // Set up SIGINT handler for clean exit
1141
+ const cleanupAndExit = () => {
1142
+ cleanup();
1143
+
1144
+ // Stop animation
1145
+ clearInterval(interval);
1146
+
1147
+ // Move cursor to start of output, clear all lines
1148
+ if (linesRendered > 0) {
1149
+ process.stderr.write(`\x1b[${linesRendered}A`);
1150
+ }
1151
+ process.stderr.write('\x1b[J'); // Clear from cursor to end of screen
1152
+ process.stderr.write('\x1B[?25h'); // Show cursor
1153
+
1154
+ process.exit(130); // Standard exit code for SIGINT
1155
+ };
1156
+
1157
+ process.on('SIGINT', cleanupAndExit);
1158
+
943
1159
  try {
1160
+ // For countdown, set up timer tracking and optional keyboard listener
1161
+ if (options.type === 'countdown') {
1162
+ const startTime = Date.now();
1163
+ remainingTime = options.timeoutMs / 1000;
1164
+ countdownInterval = setInterval(() => {
1165
+ const elapsed = Date.now() - startTime;
1166
+ remainingTime = Math.max(0, (options.timeoutMs - elapsed) / 1000);
1167
+ }, 100);
1168
+
1169
+ // Set up Enter key listener if callback provided
1170
+ if (options.onEnterPress && process.stdin.isTTY) {
1171
+ process.stdin.setRawMode(true);
1172
+ process.stdin.resume();
1173
+
1174
+ keypressListener = (chunk: Buffer) => {
1175
+ const key = chunk.toString();
1176
+ // Check for Enter key (both \r and \n)
1177
+ if (key === '\r' || key === '\n') {
1178
+ options.onEnterPress!();
1179
+ }
1180
+ // Check for Ctrl+C - let it propagate as SIGINT
1181
+ if (key === '\x03') {
1182
+ process.kill(process.pid, 'SIGINT');
1183
+ }
1184
+ };
1185
+
1186
+ process.stdin.on('data', keypressListener);
1187
+ }
1188
+ }
1189
+
944
1190
  // Execute callback
945
1191
  const result =
946
- options.type === 'progress'
947
- ? await options.callback(progressCallback)
948
- : typeof options.callback === 'function'
949
- ? await options.callback()
950
- : await options.callback;
1192
+ options.type === 'countdown'
1193
+ ? await options.callback()
1194
+ : options.type === 'progress'
1195
+ ? await options.callback(progressCallback)
1196
+ : options.type === 'logger'
1197
+ ? await options.callback(logCallback)
1198
+ : typeof options.callback === 'function'
1199
+ ? await options.callback()
1200
+ : await options.callback;
1201
+
1202
+ cleanup();
951
1203
 
952
1204
  // Stop animation first
953
1205
  clearInterval(interval);
954
1206
 
955
- // Restore cursor position, clear to end of screen, show cursor
956
- // This removes spinner and any partial output that happened during animation
957
- process.stderr.write('\x1B[u\x1B[J\x1B[?25h');
1207
+ // Move cursor to start of output, clear all lines
1208
+ if (linesRendered > 0) {
1209
+ process.stderr.write(`\x1b[${linesRendered}A`);
1210
+ }
1211
+ process.stderr.write('\x1b[J'); // Clear from cursor to end of screen
1212
+ process.stderr.write('\x1B[?25h'); // Show cursor
958
1213
 
959
1214
  // If clearOnSuccess is false, show success message
960
1215
  if (!options.clearOnSuccess) {
@@ -965,20 +1220,32 @@ export async function spinner<T>(
965
1220
 
966
1221
  return result;
967
1222
  } catch (err) {
1223
+ cleanup();
1224
+
968
1225
  // Stop animation first
969
1226
  clearInterval(interval);
970
1227
 
971
- // Restore cursor position, clear to end of screen, show cursor
972
- process.stderr.write('\x1B[u\x1B[J\x1B[?25h');
1228
+ // Move cursor to start of output, clear all lines
1229
+ if (linesRendered > 0) {
1230
+ process.stderr.write(`\x1b[${linesRendered}A`);
1231
+ }
1232
+ process.stderr.write('\x1b[J'); // Clear from cursor to end of screen
1233
+ process.stderr.write('\x1B[?25h'); // Show cursor
973
1234
 
974
1235
  // Show error
975
1236
  const errorColor = getColor('error');
976
- console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
1237
+ const errorMessage = err instanceof Error ? err.message : String(err);
1238
+ console.error(`${errorColor}${ICONS.error} ${message}: ${errorMessage}${reset}`);
977
1239
 
978
1240
  throw err;
979
1241
  }
980
1242
  }
981
1243
 
1244
+ /**
1245
+ * Alias for spinner function (for better semantics when using progress/logger types)
1246
+ */
1247
+ export const progress = spinner;
1248
+
982
1249
  /**
983
1250
  * Options for running a command with streaming output
984
1251
  */
@@ -1072,11 +1339,8 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
1072
1339
  const maxLineWidth = Math.min(80, termWidth);
1073
1340
 
1074
1341
  // Truncate command if needed
1075
- let displayCmd = command;
1076
- if (getDisplayWidth(displayCmd) > maxCmdWidth) {
1077
- // Simple truncation for now - could be smarter about this
1078
- displayCmd = displayCmd.slice(0, maxCmdWidth - 3) + '...';
1079
- }
1342
+ const displayCmd =
1343
+ getDisplayWidth(command) > maxCmdWidth ? truncateToWidth(command, maxCmdWidth) : command;
1080
1344
 
1081
1345
  // Store all output lines, display subset based on context
1082
1346
  const allOutputLines: string[] = [];
@@ -1100,11 +1364,8 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
1100
1364
 
1101
1365
  // Render output lines
1102
1366
  for (const line of displayLines) {
1103
- // Truncate line if needed
1104
- let displayLine = line;
1105
- if (getDisplayWidth(displayLine) > maxLineWidth) {
1106
- displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
1107
- }
1367
+ const displayLine =
1368
+ getDisplayWidth(line) > maxLineWidth ? truncateToWidth(line, maxLineWidth) : line;
1108
1369
  process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
1109
1370
  }
1110
1371
 
@@ -1198,10 +1459,10 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
1198
1459
  // Show final output lines
1199
1460
  const finalOutputLines = allOutputLines.slice(-finalLinesToShow);
1200
1461
  for (const line of finalOutputLines) {
1201
- let displayLine = line;
1202
- if (truncate && getDisplayWidth(displayLine) > maxLineWidth) {
1203
- displayLine = displayLine.slice(0, maxLineWidth - 3) + '...';
1204
- }
1462
+ const displayLine =
1463
+ truncate && getDisplayWidth(line) > maxLineWidth
1464
+ ? truncateToWidth(line, maxLineWidth)
1465
+ : line;
1205
1466
  process.stdout.write(`\r\x1b[K${mutedColor}${displayLine}${reset}\n`);
1206
1467
  }
1207
1468