padrone 1.5.0 → 1.7.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.
- package/CHANGELOG.md +44 -0
- package/README.md +15 -11
- package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.mjs +4 -4
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +4 -4
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +10 -12
- package/dist/docs/index.mjs.map +1 -1
- package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
- package/dist/errors-DA4KzK1M.mjs.map +1 -0
- package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
- package/dist/help-BtxLgrF_.mjs.map +1 -0
- package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
- package/dist/index-D6-7dz0l.d.mts.map +1 -0
- package/dist/index.d.mts +869 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3884 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
- package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
- package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
- package/dist/serve-YVTPzBCl.mjs.map +1 -0
- package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +2 -13
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +2 -2
- package/dist/zod.d.mts.map +1 -1
- package/dist/zod.mjs +2 -2
- package/dist/zod.mjs.map +1 -1
- package/package.json +15 -12
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -10
- package/src/cli/doctor.ts +22 -18
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +10 -7
- package/src/cli/link.ts +20 -16
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/schema-to-code.ts +2 -2
- package/src/{args.ts → core/args.ts} +32 -225
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +301 -0
- package/src/core/default-runtime.ts +239 -0
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +12 -13
- package/src/extension/auto-output.ts +146 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +44 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +262 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +12 -12
- package/src/{interactive.ts → feature/interactive.ts} +4 -4
- package/src/{mcp.ts → feature/mcp.ts} +12 -15
- package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
- package/src/{serve.ts → feature/serve.ts} +11 -15
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +10 -8
- package/src/index.ts +115 -30
- package/src/{formatter.ts → output/formatter.ts} +124 -176
- package/src/{help.ts → output/help.ts} +22 -8
- package/src/output/output-indicator.ts +87 -0
- package/src/output/primitives.ts +335 -0
- package/src/output/styling.ts +221 -0
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -276
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +718 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +60 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/{stream.ts → util/stream.ts} +27 -1
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +71 -33
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -50
- package/dist/args-D5PNDyNu.mjs.map +0 -1
- package/dist/chunk-CjcI7cDX.mjs +0 -15
- package/dist/command-utils-B1D-HqCd.mjs +0 -1117
- package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/errors-BiVrBgi6.mjs.map +0 -1
- package/dist/formatter-DtHzbP22.d.mts.map +0 -1
- package/dist/help-bbmu9-qd.mjs.map +0 -1
- package/dist/mcp-mLWIdUIu.mjs.map +0 -1
- package/dist/serve-B0u43DK7.mjs.map +0 -1
- package/dist/stream-BcC146Ud.mjs.map +0 -1
- package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
- package/dist/update-check-CFX1FV3v.mjs.map +0 -1
- package/src/command-utils.ts +0 -882
- package/src/create.ts +0 -1829
- package/src/runtime.ts +0 -497
- package/src/types.ts +0 -1291
- package/src/utils.ts +0 -140
- /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
2
|
+
import type { PadroneBarConfig, PadroneProgressIndicator, PadroneProgressOptions, PadroneSpinnerConfig } from '../core/runtime.ts';
|
|
3
|
+
import type { AnyPadroneBuilder, CommandTypesBase } from '../types/index.ts';
|
|
4
|
+
import type { WithInterceptor } from '../util/type-utils.ts';
|
|
5
|
+
import type { PadroneProgressRenderer } from './progress-renderer.ts';
|
|
6
|
+
import { createTerminalProgress } from './progress-renderer.ts';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** A progress message value: a plain string, `null` to suppress, or an object with a message and custom indicator icon. */
|
|
13
|
+
export type PadroneProgressMessage = string | null | { message?: string | null; indicator?: string };
|
|
14
|
+
|
|
15
|
+
/** Per-phase message configuration for progress indicators. */
|
|
16
|
+
export type PadroneProgressMessages<TRes = unknown> = {
|
|
17
|
+
/** Message shown during async validation. Defaults to `''` (spinner only). */
|
|
18
|
+
validation?: string;
|
|
19
|
+
/** Message shown while the command's action is running. */
|
|
20
|
+
progress?: string;
|
|
21
|
+
/** Message shown when the command succeeds. `null` to suppress. Defaults to the `progress` message. */
|
|
22
|
+
success?: PadroneProgressMessage | ((result: TRes) => PadroneProgressMessage);
|
|
23
|
+
/** Message shown when the command fails. `null` to suppress. Defaults to the error message. */
|
|
24
|
+
error?: PadroneProgressMessage | ((error: unknown) => PadroneProgressMessage);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Progress indicator configuration with messages, visual options, and renderer.
|
|
29
|
+
*/
|
|
30
|
+
export type PadroneProgressConfig<TRes = unknown> = {
|
|
31
|
+
/** Per-phase messages. A string sets the `progress` message; an object configures individual phases. */
|
|
32
|
+
message?: string | PadroneProgressMessages<TRes>;
|
|
33
|
+
/** Spinner configuration. Default `show` is `'auto'` (visible when bar is not shown). `true` forces spinner to always show (even alongside a bar). */
|
|
34
|
+
spinner?: PadroneSpinnerConfig;
|
|
35
|
+
/** Enable a progress bar. `true` for defaults (`show: 'always'`), or a `PadroneBarConfig` object. `false` to disable entirely. When omitted, bar defaults to `show: 'auto'`. */
|
|
36
|
+
bar?: boolean | PadroneBarConfig;
|
|
37
|
+
/** Show elapsed time since the indicator started. Can also be started on demand via `update({ time: true })`. */
|
|
38
|
+
time?: boolean;
|
|
39
|
+
/** Show estimated time remaining based on progress rate. Requires numeric `update()` calls. */
|
|
40
|
+
eta?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Custom renderer factory. Called to create the progress indicator.
|
|
43
|
+
* Defaults to the built-in terminal renderer (`createTerminalProgress`).
|
|
44
|
+
*/
|
|
45
|
+
renderer?: PadroneProgressRenderer;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Shared progress defaults that can be provided via context instead of repeating
|
|
50
|
+
* at each call site. Per-instance message fields are excluded — those always come
|
|
51
|
+
* from the constructor argument.
|
|
52
|
+
*
|
|
53
|
+
* Provide via context as `{ progressConfig: PadroneProgressDefaults }`.
|
|
54
|
+
*/
|
|
55
|
+
export type PadroneProgressDefaults = Pick<PadroneProgressConfig, 'message' | 'spinner' | 'bar' | 'time' | 'eta' | 'renderer'>;
|
|
56
|
+
|
|
57
|
+
/** Builder/program type after applying `padroneProgress()`. Adds `{ progress: PadroneProgressIndicator }` to the command context. */
|
|
58
|
+
export type WithProgress<T> = WithInterceptor<T, { progress: PadroneProgressIndicator }>;
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Internal helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const noopIndicator: PadroneProgressIndicator = {
|
|
65
|
+
update() {},
|
|
66
|
+
succeed() {},
|
|
67
|
+
fail() {},
|
|
68
|
+
stop() {},
|
|
69
|
+
pause() {},
|
|
70
|
+
resume() {},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function resolveMessage(field: unknown, value: unknown, fallback?: string): { message: string | null | undefined; indicator?: string } {
|
|
74
|
+
const raw = typeof field === 'function' ? (field as (v: unknown) => unknown)(value) : field;
|
|
75
|
+
if (raw === undefined) return { message: fallback };
|
|
76
|
+
if (raw === null || typeof raw === 'string') return { message: raw };
|
|
77
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
78
|
+
const obj = raw as { message?: string | null; indicator?: string };
|
|
79
|
+
return { message: obj.message, indicator: obj.indicator };
|
|
80
|
+
}
|
|
81
|
+
return { message: fallback };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cleanup(
|
|
85
|
+
indicator: PadroneProgressIndicator,
|
|
86
|
+
successConfig: unknown,
|
|
87
|
+
errorConfig: unknown,
|
|
88
|
+
error: unknown,
|
|
89
|
+
result: unknown,
|
|
90
|
+
isError: boolean,
|
|
91
|
+
) {
|
|
92
|
+
if (isError) {
|
|
93
|
+
const fallback = error instanceof Error ? error.message : String(error);
|
|
94
|
+
const { message: errorMsg, indicator: errorIcon } = resolveMessage(errorConfig, error, fallback);
|
|
95
|
+
indicator.fail(errorMsg, errorIcon !== undefined ? { indicator: errorIcon } : undefined);
|
|
96
|
+
} else {
|
|
97
|
+
const { message: successMsg, indicator: successIcon } = resolveMessage(successConfig, result);
|
|
98
|
+
indicator.succeed(successMsg, successIcon !== undefined ? { indicator: successIcon } : undefined);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Interceptor
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function resolveMessages(raw: string | PadroneProgressMessages | undefined): {
|
|
107
|
+
progress: string;
|
|
108
|
+
validation: string;
|
|
109
|
+
success: unknown;
|
|
110
|
+
error: unknown;
|
|
111
|
+
} {
|
|
112
|
+
if (!raw || typeof raw === 'string') {
|
|
113
|
+
return { progress: raw || 'Working...', validation: '', success: undefined, error: undefined };
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
progress: raw.progress ?? 'Working...',
|
|
117
|
+
validation: raw.validation ?? '',
|
|
118
|
+
success: raw.success,
|
|
119
|
+
error: raw.error,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeMessages(
|
|
124
|
+
cmd: ReturnType<typeof resolveMessages>,
|
|
125
|
+
ctx: ReturnType<typeof resolveMessages>,
|
|
126
|
+
cmdRaw: string | PadroneProgressMessages | undefined,
|
|
127
|
+
): ReturnType<typeof resolveMessages> {
|
|
128
|
+
// String shorthand: all fields come from the string, no context fallback for individual fields
|
|
129
|
+
if (typeof cmdRaw === 'string') return cmd;
|
|
130
|
+
// Object: per-field fallback to context
|
|
131
|
+
const obj = (typeof cmdRaw === 'object' ? cmdRaw : undefined) as PadroneProgressMessages | undefined;
|
|
132
|
+
return {
|
|
133
|
+
progress: obj?.progress !== undefined ? cmd.progress : (ctx.progress ?? cmd.progress),
|
|
134
|
+
validation: obj?.validation !== undefined ? cmd.validation : ctx.validation || cmd.validation,
|
|
135
|
+
success: obj?.success !== undefined ? cmd.success : (ctx.success ?? cmd.success),
|
|
136
|
+
error: obj?.error !== undefined ? cmd.error : (ctx.error ?? cmd.error),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function progressInterceptor(config: string | PadroneProgressConfig) {
|
|
141
|
+
const isObj = typeof config === 'object';
|
|
142
|
+
const rawMessage = typeof config === 'string' ? config : isObj ? config.message : undefined;
|
|
143
|
+
// Raw constructor values — undefined means "not set by caller"
|
|
144
|
+
const rawSpinner = isObj ? config.spinner : undefined;
|
|
145
|
+
const rawBar = isObj ? config.bar : undefined;
|
|
146
|
+
const rawRenderer = isObj ? config.renderer : undefined;
|
|
147
|
+
const rawTime = isObj ? config.time : undefined;
|
|
148
|
+
const rawEta = isObj ? config.eta : undefined;
|
|
149
|
+
|
|
150
|
+
return defineInterceptor({ id: 'padrone:progress', name: 'padrone:progress' })
|
|
151
|
+
.requires<{ progressConfig?: PadroneProgressDefaults }>()
|
|
152
|
+
.factory(() => {
|
|
153
|
+
let indicator: PadroneProgressIndicator | undefined;
|
|
154
|
+
let restoreOutput: (() => void) | undefined;
|
|
155
|
+
// Lazily resolved from context + constructor args
|
|
156
|
+
let resolvedRenderer: PadroneProgressRenderer | undefined;
|
|
157
|
+
let resolvedOptions: PadroneProgressOptions | undefined;
|
|
158
|
+
let msgs: ReturnType<typeof resolveMessages> | undefined;
|
|
159
|
+
|
|
160
|
+
function resolve(ctx: { context?: { progressConfig?: PadroneProgressDefaults } }) {
|
|
161
|
+
if (resolvedRenderer) return;
|
|
162
|
+
const ctxCfg = (ctx.context as Record<string, unknown> | undefined)?.progressConfig as PadroneProgressDefaults | undefined;
|
|
163
|
+
const spinner = rawSpinner ?? ctxCfg?.spinner;
|
|
164
|
+
const bar = rawBar ?? ctxCfg?.bar;
|
|
165
|
+
const time = rawTime ?? ctxCfg?.time;
|
|
166
|
+
const eta = rawEta ?? ctxCfg?.eta;
|
|
167
|
+
resolvedRenderer = rawRenderer ?? ctxCfg?.renderer ?? createTerminalProgress;
|
|
168
|
+
resolvedOptions =
|
|
169
|
+
spinner !== undefined || bar !== undefined || time !== undefined || eta !== undefined ? { spinner, bar, time, eta } : undefined;
|
|
170
|
+
msgs = mergeMessages(resolveMessages(rawMessage), resolveMessages(ctxCfg?.message), rawMessage);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const teardown = () => {
|
|
174
|
+
restoreOutput?.();
|
|
175
|
+
indicator = undefined;
|
|
176
|
+
restoreOutput = undefined;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
validate(ctx, next) {
|
|
181
|
+
resolve(ctx);
|
|
182
|
+
indicator = resolvedRenderer!(msgs!.validation || msgs!.progress, resolvedOptions);
|
|
183
|
+
|
|
184
|
+
const originalOutput = ctx.runtime.output;
|
|
185
|
+
const originalError = ctx.runtime.error;
|
|
186
|
+
ctx.runtime.output = (...args: unknown[]) => {
|
|
187
|
+
indicator!.pause();
|
|
188
|
+
originalOutput(...args);
|
|
189
|
+
indicator!.resume();
|
|
190
|
+
};
|
|
191
|
+
ctx.runtime.error = (text: string) => {
|
|
192
|
+
indicator!.pause();
|
|
193
|
+
originalError(text);
|
|
194
|
+
indicator!.resume();
|
|
195
|
+
};
|
|
196
|
+
restoreOutput = () => {
|
|
197
|
+
ctx.runtime.output = originalOutput;
|
|
198
|
+
ctx.runtime.error = originalError;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return next();
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
execute(ctx, next) {
|
|
205
|
+
// Transition from validation message to progress message
|
|
206
|
+
if (indicator && msgs!.validation) indicator.update(msgs!.progress);
|
|
207
|
+
|
|
208
|
+
const effectiveIndicator = indicator ?? noopIndicator;
|
|
209
|
+
|
|
210
|
+
const onSuccess = (value: unknown) => {
|
|
211
|
+
cleanup(effectiveIndicator, msgs!.success, msgs!.error, undefined, value, false);
|
|
212
|
+
teardown();
|
|
213
|
+
};
|
|
214
|
+
const onError = (err: unknown) => {
|
|
215
|
+
if (indicator) {
|
|
216
|
+
cleanup(indicator, msgs!.success, msgs!.error, err, undefined, true);
|
|
217
|
+
teardown();
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let result: any;
|
|
223
|
+
try {
|
|
224
|
+
result = next({ context: { ...(ctx.context as any), progress: effectiveIndicator } });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
onError(err);
|
|
227
|
+
}
|
|
228
|
+
if (result instanceof Promise) {
|
|
229
|
+
return result.then(
|
|
230
|
+
(r) => {
|
|
231
|
+
if (r.result instanceof Promise) {
|
|
232
|
+
return {
|
|
233
|
+
result: r.result.then(
|
|
234
|
+
(value: unknown) => {
|
|
235
|
+
onSuccess(value);
|
|
236
|
+
return value;
|
|
237
|
+
},
|
|
238
|
+
(err: unknown) => onError(err),
|
|
239
|
+
),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
onSuccess(r.result);
|
|
243
|
+
return r;
|
|
244
|
+
},
|
|
245
|
+
(err: unknown) => onError(err),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (result!.result instanceof Promise) {
|
|
249
|
+
return {
|
|
250
|
+
result: result!.result.then(
|
|
251
|
+
(value: unknown) => {
|
|
252
|
+
onSuccess(value);
|
|
253
|
+
return value;
|
|
254
|
+
},
|
|
255
|
+
(err: unknown) => onError(err),
|
|
256
|
+
),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
onSuccess(result!.result);
|
|
260
|
+
return result;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
})
|
|
264
|
+
.provides<{ progress: PadroneProgressIndicator }>();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Extension
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Extension that adds an auto-managed progress indicator to the command pipeline.
|
|
273
|
+
*
|
|
274
|
+
* - `string` — a single message used for all states.
|
|
275
|
+
* - `PadroneProgressConfig` — separate messages for validation, progress, success, and error.
|
|
276
|
+
*
|
|
277
|
+
* The indicator is automatically started before validation, updated at each phase transition,
|
|
278
|
+
* and stopped on success (`.succeed()`) or failure (`.fail()`).
|
|
279
|
+
*
|
|
280
|
+
* Provides `{ progress: PadroneProgressIndicator }` on the command context.
|
|
281
|
+
* Access it in action handlers as `ctx.context.progress`.
|
|
282
|
+
*
|
|
283
|
+
* Uses the built-in terminal renderer by default. Pass a custom `renderer` for non-terminal
|
|
284
|
+
* environments (web UIs, testing, etc).
|
|
285
|
+
*
|
|
286
|
+
* Usage:
|
|
287
|
+
* ```ts
|
|
288
|
+
* createPadrone('my-cli')
|
|
289
|
+
* .command('sync', (c) =>
|
|
290
|
+
* c.extend(padroneProgress('Syncing...'))
|
|
291
|
+
* .action((_args, ctx) => {
|
|
292
|
+
* ctx.context.progress.update('halfway');
|
|
293
|
+
* })
|
|
294
|
+
* )
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export function padroneProgress<T extends CommandTypesBase>(config?: string | PadroneProgressConfig): (builder: T) => WithProgress<T> {
|
|
298
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(progressInterceptor(config ?? 'Working...'))) as any;
|
|
299
|
+
}
|