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.
- package/CHANGELOG.md +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- 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} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- 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 +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- 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 +124 -11
- package/src/extension/auto-output.ts +95 -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 +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -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} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- 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} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -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/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.d.mts +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- 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
|
+
}
|