padrone 1.4.0 → 1.6.0

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 (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
@@ -0,0 +1,51 @@
1
+ import { resolveAllCommands } from '../core/commands.ts';
2
+ import type { AnyPadroneBuilder, CommandTypesBase, PadroneCommand } from '../types/index.ts';
3
+ import type { PadroneSchema } from '../types/schema.ts';
4
+ import type { WithCommand } from '../util/type-utils.ts';
5
+ import { getRootCommand } from '../util/utils.ts';
6
+ import { passthroughSchema } from './utils.ts';
7
+
8
+ // ── Types ────────────────────────────────────────────────────────────────
9
+
10
+ type ManArgs = { setup?: boolean; remove?: boolean };
11
+
12
+ type ManCommand = PadroneCommand<'man', '', PadroneSchema<ManArgs>, string, [], [], true>;
13
+
14
+ export type WithMan<T> = WithCommand<T, 'man', ManCommand>;
15
+
16
+ // ── Extension ────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Extension that adds the `man` command for man page generation.
20
+ *
21
+ * Usage:
22
+ * ```ts
23
+ * createPadrone('my-cli').extend(padroneMan())
24
+ * ```
25
+ */
26
+ export function padroneMan(): <T extends CommandTypesBase>(builder: T) => WithMan<T> {
27
+ return ((builder: AnyPadroneBuilder) =>
28
+ builder.command('man', (c) =>
29
+ c
30
+ .configure({ description: 'Generate man pages', hidden: true })
31
+ .arguments(passthroughSchema({ setup: 'boolean', remove: 'boolean' }))
32
+ .async()
33
+ .action(async (args, ctx) => {
34
+ const rootCommand = getRootCommand(ctx.command);
35
+ resolveAllCommands(rootCommand);
36
+ const { setupManPages, removeManPages, generateDocs } = await import('../docs/index.ts');
37
+ if (args.setup) {
38
+ const setupResult = await setupManPages(rootCommand);
39
+ return `${setupResult.updated ? 'Updated' : 'Installed'} ${setupResult.written.length} man page(s) in ${setupResult.dir}`;
40
+ }
41
+ if (args.remove) {
42
+ const removeResult = await removeManPages(rootCommand);
43
+ return removeResult.removed.length > 0
44
+ ? `Removed ${removeResult.removed.length} man page(s) from ${removeResult.dir}`
45
+ : 'No man pages found to remove.';
46
+ }
47
+ const docsResult = generateDocs(rootCommand, { format: 'man' });
48
+ return docsResult.pages[0]?.content ?? '';
49
+ }),
50
+ )) as any;
51
+ }
@@ -0,0 +1,52 @@
1
+ import { resolveAllCommands } from '../core/commands.ts';
2
+ import type { PadroneMcpPreferences } from '../feature/mcp.ts';
3
+ import type { AnyPadroneBuilder, CommandTypesBase, PadroneCommand } from '../types/index.ts';
4
+ import type { PadroneSchema } from '../types/schema.ts';
5
+ import type { WithCommand } from '../util/type-utils.ts';
6
+ import { getRootCommand } from '../util/utils.ts';
7
+ import { passthroughSchema } from './utils.ts';
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────────
10
+
11
+ type McpArgs = { transport?: string; port?: string; host?: string; basePath?: string };
12
+
13
+ type McpCommand = PadroneCommand<'mcp', '', PadroneSchema<McpArgs>, void, [], [], true>;
14
+
15
+ export type WithMcp<T> = WithCommand<T, 'mcp', McpCommand>;
16
+
17
+ // ── Extension ────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Extension that adds the `mcp` command for starting a Model Context Protocol server.
21
+ *
22
+ * Usage:
23
+ * ```ts
24
+ * createPadrone('my-cli').extend(padroneMcp())
25
+ * ```
26
+ */
27
+ export function padroneMcp(defaults?: PadroneMcpPreferences): <T extends CommandTypesBase>(builder: T) => WithMcp<T> {
28
+ return ((builder: AnyPadroneBuilder) =>
29
+ builder.command('mcp', (c) =>
30
+ c
31
+ .configure({ description: 'Start a Model Context Protocol server', hidden: true })
32
+ .arguments(passthroughSchema({ transport: 'string', port: 'string', host: 'string', 'base-path': 'string' }), {
33
+ positional: ['transport'],
34
+ })
35
+ .async()
36
+ .action(async (args, ctx) => {
37
+ const rootCommand = getRootCommand(ctx.command);
38
+ resolveAllCommands(rootCommand);
39
+ const { startMcpServer } = await import('../feature/mcp.ts');
40
+ const transport = args.transport === 'stdio' || args.transport === 'http' ? args.transport : undefined;
41
+ const port = args.port ? parseInt(args.port, 10) : undefined;
42
+ const prefs: PadroneMcpPreferences = {
43
+ ...defaults,
44
+ transport: transport ?? defaults?.transport,
45
+ port: port && !Number.isNaN(port) ? port : defaults?.port,
46
+ host: args.host ?? defaults?.host,
47
+ basePath: args['base-path'] ?? defaults?.basePath,
48
+ };
49
+ await startMcpServer(ctx.program, rootCommand, ctx.program.eval, prefs);
50
+ }),
51
+ )) as any;
52
+ }
@@ -0,0 +1,338 @@
1
+ import type {
2
+ PadroneBarConfig,
3
+ PadroneProgressIndicator,
4
+ PadroneProgressOptions,
5
+ PadroneProgressShow,
6
+ PadroneProgressUpdate,
7
+ PadroneSpinnerConfig,
8
+ PadroneSpinnerPreset,
9
+ } from '../core/runtime.ts';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Spinner presets & resolution
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const spinnerPresets: Record<PadroneSpinnerPreset, string[]> = {
16
+ dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
17
+ line: ['-', '\\', '|', '/'],
18
+ arc: ['◜', '◠', '◝', '◞', '◡', '◟'],
19
+ bounce: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'],
20
+ };
21
+
22
+ type ResolvedSpinnerConfig = { frames: string[]; interval: number; show: PadroneProgressShow };
23
+
24
+ function resolveSpinnerConfig(config?: PadroneSpinnerConfig): ResolvedSpinnerConfig {
25
+ if (config === false) return { frames: [], interval: 80, show: 'never' };
26
+ if (config === true) return { frames: spinnerPresets.dots, interval: 80, show: 'always' };
27
+ if (typeof config === 'string') return { frames: spinnerPresets[config], interval: 80, show: 'auto' };
28
+ if (typeof config === 'object') {
29
+ return {
30
+ frames: config.frames ?? spinnerPresets.dots,
31
+ interval: config.interval ?? 80,
32
+ show: config.show ?? 'auto',
33
+ };
34
+ }
35
+ return { frames: spinnerPresets.dots, interval: 80, show: 'auto' };
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Bar resolution & rendering
40
+ // ---------------------------------------------------------------------------
41
+
42
+ type ResolvedBarConfig = {
43
+ width: number;
44
+ filled: string;
45
+ empty: string;
46
+ animation: 'bounce' | 'slide' | 'pulse';
47
+ show: PadroneProgressShow;
48
+ };
49
+
50
+ const defaultBarConfig: ResolvedBarConfig = { width: 20, filled: '█', empty: '░', animation: 'bounce', show: 'auto' };
51
+
52
+ function resolveBarConfig(bar: boolean | PadroneBarConfig | undefined): ResolvedBarConfig | undefined {
53
+ if (bar === false) return undefined;
54
+ if (!bar) return { ...defaultBarConfig };
55
+ if (bar === true) return { ...defaultBarConfig, show: 'always' };
56
+ return {
57
+ width: bar.width ?? 20,
58
+ filled: bar.filled ?? '█',
59
+ empty: bar.empty ?? '░',
60
+ animation: bar.animation ?? 'bounce',
61
+ show: bar.show ?? 'always',
62
+ };
63
+ }
64
+
65
+ const SEGMENT_RATIO = 0.25;
66
+ const pulseGradient = ['░', '▒', '▓', '█', '▓', '▒', '░'];
67
+
68
+ function formatIndeterminate(cfg: ResolvedBarConfig, frame: number): string {
69
+ const { width, filled, empty, animation } = cfg;
70
+ const pad = ''.padStart(4);
71
+
72
+ if (animation === 'pulse') {
73
+ const idx = frame % pulseGradient.length;
74
+ return `${pad} ${pulseGradient[idx]!.repeat(width)}`;
75
+ }
76
+
77
+ const seg = Math.max(2, Math.round(width * SEGMENT_RATIO));
78
+ const travel = width - seg;
79
+
80
+ if (animation === 'slide') {
81
+ const offset = frame % (travel + 1);
82
+ return `${pad} ${empty.repeat(offset)}${filled.repeat(seg)}${empty.repeat(travel - offset)}`;
83
+ }
84
+
85
+ // bounce (default)
86
+ const cycle = travel * 2;
87
+ const pos = frame % cycle;
88
+ const offset = pos <= travel ? pos : cycle - pos;
89
+ return `${pad} ${empty.repeat(offset)}${filled.repeat(seg)}${empty.repeat(width - offset - seg)}`;
90
+ }
91
+
92
+ function formatBar(progress: number | undefined, cfg: ResolvedBarConfig, frame: number): string {
93
+ if (progress === undefined) return formatIndeterminate(cfg, frame);
94
+ const { width, filled, empty } = cfg;
95
+ const clamped = Math.max(0, Math.min(1, progress));
96
+ const filledCount = Math.round(clamped * width);
97
+ const pct = `${Math.round(clamped * 100)}%`.padStart(4);
98
+ return `${pct} ${filled.repeat(filledCount)}${empty.repeat(width - filledCount)}`;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Helpers
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function parseUpdate(value: PadroneProgressUpdate): { message?: string; progress?: number; indeterminate?: boolean; time?: boolean } {
106
+ if (typeof value === 'string') return { message: value };
107
+ if (typeof value === 'number') return { progress: value };
108
+ return value;
109
+ }
110
+
111
+ function formatDuration(ms: number): string {
112
+ const totalSeconds = Math.floor(ms / 1000);
113
+ const hours = Math.floor(totalSeconds / 3600);
114
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
115
+ const seconds = totalSeconds % 60;
116
+ if (hours > 0) return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
117
+ return `${minutes}:${String(seconds).padStart(2, '0')}`;
118
+ }
119
+
120
+ function estimateEta(samples: { time: number; progress: number }[]): number | undefined {
121
+ if (samples.length < 2) return undefined;
122
+ const first = samples[0]!;
123
+ const last = samples[samples.length - 1]!;
124
+ const progressDelta = last.progress - first.progress;
125
+ if (progressDelta <= 0) return undefined;
126
+ const timeDelta = last.time - first.time;
127
+ const rate = progressDelta / timeDelta;
128
+ const remaining = 1 - last.progress;
129
+ if (remaining <= 0) return 0;
130
+ return remaining / rate;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Factory type
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /** Factory function that creates a `PadroneProgressIndicator`. */
138
+ export type PadroneProgressRenderer = (message: string, options?: PadroneProgressOptions) => PadroneProgressIndicator;
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Default terminal renderer
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Creates a terminal progress indicator (spinner, bar, or both).
146
+ * Returns a no-op indicator in non-TTY/CI environments.
147
+ */
148
+ export function createTerminalProgress(message: string, options?: PadroneProgressOptions): PadroneProgressIndicator {
149
+ const spinnerCfg = resolveSpinnerConfig(options?.spinner);
150
+ const successIcon = options?.successIndicator ?? '✔';
151
+ const errorIcon = options?.errorIndicator ?? '✖';
152
+ const barCfg = resolveBarConfig(options?.bar);
153
+
154
+ const formatFinal = (icon: string, msg: string) => (icon ? `${icon} ${msg}\n` : `${msg}\n`);
155
+
156
+ if (typeof process === 'undefined' || !process.stderr?.isTTY) {
157
+ return {
158
+ update() {},
159
+ succeed(msg, opts) {
160
+ if (msg === null) return;
161
+ const icon = opts?.indicator ?? successIcon;
162
+ if (msg || message) process?.stderr?.write?.(formatFinal(icon, msg || message));
163
+ },
164
+ fail(msg, opts) {
165
+ if (msg === null) return;
166
+ const icon = opts?.indicator ?? errorIcon;
167
+ if (msg || message) process?.stderr?.write?.(formatFinal(icon, msg || message));
168
+ },
169
+ stop() {},
170
+ pause() {},
171
+ resume() {},
172
+ };
173
+ }
174
+
175
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping requires matching ESC
176
+ const ansiPattern = /\x1b\[[0-9;]*m/g;
177
+
178
+ if (spinnerCfg.show === 'never' && (!barCfg || barCfg.show === 'never') && !message) {
179
+ return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
180
+ }
181
+
182
+ const showTime = options?.time ?? false;
183
+ const showEta = options?.eta ?? false;
184
+
185
+ let spinnerFrame = 0;
186
+ let barFrame = 0;
187
+ let text = message;
188
+ let progress: number | undefined;
189
+ let indeterminate = false;
190
+ let stopped = false;
191
+ let paused = false;
192
+ let timeEnabled = showTime;
193
+ let startTime = showTime ? Date.now() : 0;
194
+ const etaSamples: { time: number; progress: number }[] = [];
195
+ let etaMs: number | undefined;
196
+ let etaCalculatedAt = 0;
197
+
198
+ const writeStderr = process.stderr.write.bind(process.stderr);
199
+ const writeStdout = process.stdout.write.bind(process.stdout);
200
+ let prevLineCount = 0;
201
+
202
+ const clearLines = () => {
203
+ if (prevLineCount > 1) {
204
+ // Move cursor up and clear each wrapped line above the current one
205
+ for (let i = 1; i < prevLineCount; i++) writeStderr('\x1b[1A\x1b[2K');
206
+ }
207
+ writeStderr('\x1b[2K\r');
208
+ prevLineCount = 0;
209
+ };
210
+
211
+ /** Count how many terminal rows `str` occupies, accounting for line wrapping. */
212
+ const lineCount = (str: string): number => {
213
+ const cols = process.stderr.columns || 80;
214
+ // Strip ANSI escape sequences for accurate width measurement
215
+ const visible = str.replace(ansiPattern, '');
216
+ return Math.max(1, Math.ceil(visible.length / cols));
217
+ };
218
+
219
+ const render = () => {
220
+ if (paused || stopped) return;
221
+
222
+ const barVisible = barCfg && (barCfg.show === 'always' || (barCfg.show === 'auto' && (progress !== undefined || indeterminate)));
223
+ const spinnerVisible = spinnerCfg.show === 'always' || (spinnerCfg.show === 'auto' && !barVisible);
224
+
225
+ let line = '';
226
+ if (barVisible) line += formatBar(progress, barCfg!, barFrame);
227
+ const hasEta = showEta && progress !== undefined && progress < 1 && etaMs !== undefined;
228
+ if (timeEnabled || hasEta) {
229
+ const parts: string[] = [];
230
+ if (timeEnabled) parts.push(`⏱ ${formatDuration(Date.now() - startTime)}`);
231
+ if (hasEta) {
232
+ const elapsed = Date.now() - etaCalculatedAt;
233
+ parts.push(`ETA ${formatDuration(Math.max(0, etaMs! - elapsed))}`);
234
+ }
235
+ if (line) line += ' ';
236
+ line += parts.join(' | ');
237
+ }
238
+ if (spinnerVisible) {
239
+ if (line) line += ' ';
240
+ line += frames[spinnerFrame] ?? '';
241
+ }
242
+ if (text) {
243
+ if (line) line += ' ';
244
+ line += text;
245
+ }
246
+
247
+ if (line) {
248
+ clearLines();
249
+ writeStderr(line);
250
+ prevLineCount = lineCount(line);
251
+ } else {
252
+ clearLines();
253
+ }
254
+ };
255
+
256
+ const { frames } = spinnerCfg;
257
+ const needsAnimation = spinnerCfg.show !== 'never' || (barCfg && barCfg.show !== 'never');
258
+ const tickInterval = barCfg && barCfg.show !== 'never' ? Math.min(80, spinnerCfg.interval) : spinnerCfg.interval;
259
+
260
+ const timer = needsAnimation
261
+ ? setInterval(() => {
262
+ spinnerFrame = (spinnerFrame + 1) % (frames.length || 1);
263
+ barFrame++;
264
+ render();
265
+ }, tickInterval)
266
+ : undefined;
267
+
268
+ render();
269
+
270
+ const clear = () => {
271
+ if (stopped) return;
272
+ stopped = true;
273
+ paused = false;
274
+ if (timer) clearInterval(timer);
275
+ clearLines();
276
+ };
277
+
278
+ return {
279
+ update(value) {
280
+ if (stopped) return;
281
+ const parsed = parseUpdate(value);
282
+ if (parsed.message !== undefined) text = parsed.message;
283
+ if (parsed.progress !== undefined) {
284
+ progress = parsed.progress;
285
+ if (showEta) {
286
+ const now = Date.now();
287
+ etaSamples.push({ time: now, progress: parsed.progress });
288
+ const estimated = estimateEta(etaSamples);
289
+ if (estimated !== undefined) {
290
+ etaMs = estimated;
291
+ etaCalculatedAt = now;
292
+ }
293
+ }
294
+ }
295
+ if (parsed.indeterminate !== undefined) {
296
+ indeterminate = parsed.indeterminate;
297
+ if (indeterminate) progress = undefined;
298
+ }
299
+ if (parsed.time !== undefined) {
300
+ if (parsed.time && !timeEnabled) {
301
+ timeEnabled = true;
302
+ startTime = Date.now();
303
+ } else if (!parsed.time) {
304
+ timeEnabled = false;
305
+ }
306
+ }
307
+ render();
308
+ },
309
+ succeed(msg, opts) {
310
+ clear();
311
+ if (msg === null) return;
312
+ const finalMsg = msg ?? text;
313
+ const icon = opts?.indicator ?? successIcon;
314
+ if (finalMsg) writeStderr(formatFinal(icon, finalMsg));
315
+ },
316
+ fail(msg, opts) {
317
+ clear();
318
+ if (msg === null) return;
319
+ const finalMsg = msg ?? text;
320
+ const icon = opts?.indicator ?? errorIcon;
321
+ if (finalMsg) writeStderr(formatFinal(icon, finalMsg));
322
+ },
323
+ stop() {
324
+ clear();
325
+ },
326
+ pause() {
327
+ if (stopped || paused) return;
328
+ paused = true;
329
+ clearLines();
330
+ writeStdout('\x1b[2K\r');
331
+ },
332
+ resume() {
333
+ if (stopped || !paused) return;
334
+ paused = false;
335
+ render();
336
+ },
337
+ };
338
+ }