Skip to content

Commit d76cc44

Browse files
committed
feat: add silent mode and eta helpers to progress
1 parent a72cae7 commit d76cc44

4 files changed

Lines changed: 59 additions & 15 deletions

File tree

packages/padrone/src/core/runtime.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ export type PadroneProgress = {
3131
succeed: (message?: string | null, options?: { indicator?: string }) => void;
3232
/** Mark as failed and stop. Pass `null` to stop without rendering a final message. */
3333
fail: (message?: string | null, options?: { indicator?: string }) => void;
34+
/** Control ETA (estimated time remaining) display at runtime. */
35+
eta: {
36+
/** Enable ETA tracking. Starts collecting samples from subsequent `update()` calls. */
37+
start: () => void;
38+
/** Disable ETA display. */
39+
stop: () => void;
40+
/** Clear collected samples and restart estimation. Useful between operation phases. */
41+
reset: () => void;
42+
};
3443
/** Stop without success/fail status. */
3544
stop: () => void;
3645
/** Temporarily hide the indicator so other output can be written cleanly. */

packages/padrone/src/extension/progress-renderer.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ export function createTerminalProgress(message: string, options?: PadroneProgres
154154
const formatFinal = (icon: string, msg: string) => (icon ? `${icon} ${msg}\n` : `${msg}\n`);
155155

156156
if (typeof process === 'undefined' || !process.stderr?.isTTY) {
157+
const noopEta = { start() {}, stop() {}, reset() {} };
157158
return {
158159
update() {},
160+
eta: noopEta,
159161
succeed(msg, opts) {
160162
if (msg === null) return;
161163
const icon = opts?.indicator ?? successIcon;
@@ -176,11 +178,11 @@ export function createTerminalProgress(message: string, options?: PadroneProgres
176178
const ansiPattern = /\x1b\[[0-9;]*m/g;
177179

178180
if (spinnerCfg.show === 'never' && (!barCfg || barCfg.show === 'never') && !message) {
179-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
181+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
180182
}
181183

182184
const showTime = options?.time ?? false;
183-
const showEta = options?.eta ?? false;
185+
let etaEnabled = options?.eta ?? false;
184186

185187
let spinnerFrame = 0;
186188
let barFrame = 0;
@@ -224,7 +226,7 @@ export function createTerminalProgress(message: string, options?: PadroneProgres
224226

225227
let line = '';
226228
if (barVisible) line += formatBar(progress, barCfg!, barFrame);
227-
const hasEta = showEta && progress !== undefined && progress < 1 && etaMs !== undefined;
229+
const hasEta = etaEnabled && progress !== undefined && progress < 1 && etaMs !== undefined;
228230
if (timeEnabled || hasEta) {
229231
const parts: string[] = [];
230232
if (timeEnabled) parts.push(`⏱ ${formatDuration(Date.now() - startTime)}`);
@@ -278,14 +280,35 @@ export function createTerminalProgress(message: string, options?: PadroneProgres
278280
clearLines();
279281
};
280282

283+
const eta = {
284+
start() {
285+
if (stopped) return;
286+
etaEnabled = true;
287+
render();
288+
},
289+
stop() {
290+
if (stopped) return;
291+
etaEnabled = false;
292+
etaMs = undefined;
293+
render();
294+
},
295+
reset() {
296+
if (stopped) return;
297+
etaSamples.length = 0;
298+
etaMs = undefined;
299+
etaCalculatedAt = 0;
300+
render();
301+
},
302+
};
303+
281304
return {
282305
update(value) {
283306
if (stopped) return;
284307
const parsed = parseUpdate(value);
285308
if (parsed.message !== undefined) text = parsed.message;
286309
if (parsed.progress !== undefined) {
287310
progress = parsed.progress;
288-
if (showEta) {
311+
if (etaEnabled) {
289312
const now = Date.now();
290313
etaSamples.push({ time: now, progress: parsed.progress });
291314
const estimated = estimateEta(etaSamples);
@@ -309,6 +332,7 @@ export function createTerminalProgress(message: string, options?: PadroneProgres
309332
}
310333
render();
311334
},
335+
eta,
312336
succeed(msg, opts) {
313337
clear();
314338
if (msg === null) return;

packages/padrone/src/extension/progress.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export type PadroneProgressConfig<TRes = unknown> = {
4343
* Defaults to the built-in terminal renderer (`createTerminalProgress`).
4444
*/
4545
renderer?: PadroneProgressRenderer;
46+
/** Suppress all progress output. The `progress` interface is still provided on the context as a no-op. */
47+
silent?: boolean;
4648
};
4749

4850
/**
@@ -52,7 +54,7 @@ export type PadroneProgressConfig<TRes = unknown> = {
5254
*
5355
* Provide via context as `{ progressConfig: PadroneProgressDefaults }`.
5456
*/
55-
export type PadroneProgressDefaults = Pick<PadroneProgressConfig, 'message' | 'spinner' | 'bar' | 'time' | 'eta' | 'renderer'>;
57+
export type PadroneProgressDefaults = Pick<PadroneProgressConfig, 'message' | 'spinner' | 'bar' | 'time' | 'eta' | 'renderer' | 'silent'>;
5658

5759
/** Builder/program type after applying `padroneProgress()`. Adds `{ progress: PadroneProgress }` to the command context. */
5860
export type WithProgress<T> = WithInterceptor<T, { progress: PadroneProgress }>;
@@ -61,8 +63,10 @@ export type WithProgress<T> = WithInterceptor<T, { progress: PadroneProgress }>;
6163
// Internal helpers
6264
// ---------------------------------------------------------------------------
6365

66+
const noopEta = { start() {}, stop() {}, reset() {} };
6467
const noopIndicator: PadroneProgress = {
6568
update() {},
69+
eta: noopEta,
6670
succeed() {},
6771
fail() {},
6872
stop() {},
@@ -146,6 +150,7 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
146150
const rawRenderer = isObj ? config.renderer : undefined;
147151
const rawTime = isObj ? config.time : undefined;
148152
const rawEta = isObj ? config.eta : undefined;
153+
const rawSilent = isObj ? config.silent : undefined;
149154

150155
return defineInterceptor({ id: 'padrone:progress', name: 'padrone:progress' })
151156
.requires<{ progressConfig?: PadroneProgressDefaults }>()
@@ -155,11 +160,13 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
155160
// Lazily resolved from context + constructor args
156161
let resolvedRenderer: PadroneProgressRenderer | undefined;
157162
let resolvedOptions: PadroneProgressOptions | undefined;
163+
let resolvedSilent = false;
158164
let msgs: ReturnType<typeof resolveMessages> | undefined;
159165

160166
function resolve(ctx: { context?: { progressConfig?: PadroneProgressDefaults } }) {
161167
if (resolvedRenderer) return;
162168
const ctxCfg = (ctx.context as Record<string, unknown> | undefined)?.progressConfig as PadroneProgressDefaults | undefined;
169+
resolvedSilent = rawSilent ?? ctxCfg?.silent ?? false;
163170
const spinner = rawSpinner ?? ctxCfg?.spinner;
164171
const bar = rawBar ?? ctxCfg?.bar;
165172
const time = rawTime ?? ctxCfg?.time;
@@ -179,6 +186,7 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
179186
return {
180187
validate(ctx, next) {
181188
resolve(ctx);
189+
if (resolvedSilent) return next();
182190
indicator = resolvedRenderer!(msgs!.validation || msgs!.progress, resolvedOptions);
183191

184192
const originalOutput = ctx.runtime.output;
@@ -227,6 +235,8 @@ function progressInterceptor(config: string | PadroneProgressConfig) {
227235
},
228236

229237
execute(_ctx, next) {
238+
if (resolvedSilent) return next({ context: { progress: noopIndicator } });
239+
230240
// Transition from validation message to progress message
231241
if (indicator && msgs!.validation) indicator.update(msgs!.progress);
232242

packages/padrone/tests/progress.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function createMockProgress() {
3535
stop: () => {
3636
calls.push('stop');
3737
},
38+
eta: { start() {}, stop() {}, reset() {} },
3839
pause: () => {
3940
calls.push('pause');
4041
},
@@ -571,7 +572,7 @@ describe('progress', () => {
571572
let receivedOptions: any;
572573
const renderer: PadroneProgressRenderer = (_message, options) => {
573574
receivedOptions = options;
574-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
575+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
575576
};
576577

577578
const program = createPadrone('app')
@@ -637,7 +638,7 @@ describe('progress', () => {
637638
let receivedOptions: any;
638639
const renderer: PadroneProgressRenderer = (_message, options) => {
639640
receivedOptions = options;
640-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
641+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
641642
};
642643

643644
const program = createPadrone('app').command('cmd', (c) =>
@@ -652,7 +653,7 @@ describe('progress', () => {
652653
let receivedOptions: any = 'NOT_CALLED';
653654
const renderer: PadroneProgressRenderer = (_message, options) => {
654655
receivedOptions = options;
655-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
656+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
656657
};
657658

658659
const program = createPadrone('app').command('cmd', (c) =>
@@ -667,7 +668,7 @@ describe('progress', () => {
667668
let receivedOptions: any;
668669
const renderer: PadroneProgressRenderer = (_message, options) => {
669670
receivedOptions = options;
670-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
671+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
671672
};
672673

673674
const program = createPadrone('app').command('cmd', (c) =>
@@ -684,7 +685,7 @@ describe('progress', () => {
684685
let receivedOptions: any;
685686
const renderer: PadroneProgressRenderer = (_message, options) => {
686687
receivedOptions = options;
687-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
688+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
688689
};
689690

690691
const program = createPadrone('app').command('cmd', (c) =>
@@ -699,7 +700,7 @@ describe('progress', () => {
699700
let receivedOptions: any;
700701
const renderer: PadroneProgressRenderer = (_message, options) => {
701702
receivedOptions = options;
702-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
703+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
703704
};
704705

705706
const program = createPadrone('app').command('cmd', (c) =>
@@ -757,7 +758,7 @@ describe('progress', () => {
757758
let receivedOptions: any;
758759
const renderer: PadroneProgressRenderer = (_message, options) => {
759760
receivedOptions = options;
760-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
761+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
761762
};
762763

763764
const program = createPadrone('app').command('cmd', (c) =>
@@ -772,7 +773,7 @@ describe('progress', () => {
772773
let receivedOptions: any;
773774
const renderer: PadroneProgressRenderer = (_message, options) => {
774775
receivedOptions = options;
775-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
776+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
776777
};
777778

778779
const program = createPadrone('app').command('cmd', (c) =>
@@ -787,7 +788,7 @@ describe('progress', () => {
787788
let receivedOptions: any;
788789
const renderer: PadroneProgressRenderer = (_message, options) => {
789790
receivedOptions = options;
790-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
791+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
791792
};
792793

793794
const program = createPadrone('app').command('cmd', (c) =>
@@ -802,7 +803,7 @@ describe('progress', () => {
802803
let receivedOptions: any;
803804
const renderer: PadroneProgressRenderer = (_message, options) => {
804805
receivedOptions = options;
805-
return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
806+
return { update() {}, eta: { start() {}, stop() {}, reset() {} }, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
806807
};
807808

808809
const program = createPadrone('app')

0 commit comments

Comments
 (0)