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.
Files changed (141) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +15 -11
  3. package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.mjs +4 -4
  6. package/dist/codegen/index.mjs.map +1 -1
  7. package/dist/commands-B_gufyR9.mjs +514 -0
  8. package/dist/commands-B_gufyR9.mjs.map +1 -0
  9. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
  10. package/dist/completion-BEuflbDO.mjs.map +1 -0
  11. package/dist/docs/index.d.mts +4 -4
  12. package/dist/docs/index.d.mts.map +1 -1
  13. package/dist/docs/index.mjs +10 -12
  14. package/dist/docs/index.mjs.map +1 -1
  15. package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
  16. package/dist/errors-DA4KzK1M.mjs.map +1 -0
  17. package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
  18. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  19. package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
  20. package/dist/help-BtxLgrF_.mjs.map +1 -0
  21. package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
  22. package/dist/index-D6-7dz0l.d.mts.map +1 -0
  23. package/dist/index.d.mts +869 -36
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3884 -1699
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
  28. package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
  29. package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
  30. package/dist/serve-YVTPzBCl.mjs.map +1 -0
  31. package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
  32. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  33. package/dist/test.d.mts +5 -8
  34. package/dist/test.d.mts.map +1 -1
  35. package/dist/test.mjs +2 -13
  36. package/dist/test.mjs.map +1 -1
  37. package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  38. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  39. package/dist/zod.d.mts +2 -2
  40. package/dist/zod.d.mts.map +1 -1
  41. package/dist/zod.mjs +2 -2
  42. package/dist/zod.mjs.map +1 -1
  43. package/package.json +15 -12
  44. package/src/cli/completions.ts +14 -11
  45. package/src/cli/docs.ts +13 -10
  46. package/src/cli/doctor.ts +22 -18
  47. package/src/cli/index.ts +28 -82
  48. package/src/cli/init.ts +10 -7
  49. package/src/cli/link.ts +20 -16
  50. package/src/cli/wrap.ts +14 -11
  51. package/src/codegen/schema-to-code.ts +2 -2
  52. package/src/{args.ts → core/args.ts} +32 -225
  53. package/src/core/commands.ts +373 -0
  54. package/src/core/create.ts +301 -0
  55. package/src/core/default-runtime.ts +239 -0
  56. package/src/{errors.ts → core/errors.ts} +22 -0
  57. package/src/core/exec.ts +259 -0
  58. package/src/core/interceptors.ts +302 -0
  59. package/src/{parse.ts → core/parse.ts} +36 -89
  60. package/src/core/program-methods.ts +301 -0
  61. package/src/core/results.ts +229 -0
  62. package/src/core/runtime.ts +246 -0
  63. package/src/core/validate.ts +247 -0
  64. package/src/docs/index.ts +12 -13
  65. package/src/extension/auto-output.ts +146 -0
  66. package/src/extension/color.ts +38 -0
  67. package/src/extension/completion.ts +49 -0
  68. package/src/extension/config.ts +262 -0
  69. package/src/extension/env.ts +101 -0
  70. package/src/extension/help.ts +192 -0
  71. package/src/extension/index.ts +44 -0
  72. package/src/extension/ink.ts +93 -0
  73. package/src/extension/interactive.ts +106 -0
  74. package/src/extension/logger.ts +262 -0
  75. package/src/extension/man.ts +51 -0
  76. package/src/extension/mcp.ts +52 -0
  77. package/src/extension/progress-renderer.ts +338 -0
  78. package/src/extension/progress.ts +299 -0
  79. package/src/extension/repl.ts +94 -0
  80. package/src/extension/serve.ts +48 -0
  81. package/src/extension/signal.ts +87 -0
  82. package/src/extension/stdin.ts +62 -0
  83. package/src/extension/suggestions.ts +114 -0
  84. package/src/extension/timing.ts +81 -0
  85. package/src/extension/tracing.ts +175 -0
  86. package/src/extension/update-check.ts +77 -0
  87. package/src/extension/utils.ts +51 -0
  88. package/src/extension/version.ts +63 -0
  89. package/src/{completion.ts → feature/completion.ts} +12 -12
  90. package/src/{interactive.ts → feature/interactive.ts} +4 -4
  91. package/src/{mcp.ts → feature/mcp.ts} +12 -15
  92. package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
  93. package/src/{serve.ts → feature/serve.ts} +11 -15
  94. package/src/feature/test.ts +262 -0
  95. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  96. package/src/{wrap.ts → feature/wrap.ts} +10 -8
  97. package/src/index.ts +115 -30
  98. package/src/{formatter.ts → output/formatter.ts} +124 -176
  99. package/src/{help.ts → output/help.ts} +22 -8
  100. package/src/output/output-indicator.ts +87 -0
  101. package/src/output/primitives.ts +335 -0
  102. package/src/output/styling.ts +221 -0
  103. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  104. package/src/schema/zod.ts +50 -0
  105. package/src/test.ts +2 -276
  106. package/src/types/args-meta.ts +151 -0
  107. package/src/types/builder.ts +718 -0
  108. package/src/types/command.ts +157 -0
  109. package/src/types/index.ts +60 -0
  110. package/src/types/interceptor.ts +296 -0
  111. package/src/types/preferences.ts +83 -0
  112. package/src/types/result.ts +71 -0
  113. package/src/types/schema.ts +19 -0
  114. package/src/util/dotenv.ts +244 -0
  115. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  116. package/src/{stream.ts → util/stream.ts} +27 -1
  117. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  118. package/src/{type-utils.ts → util/type-utils.ts} +71 -33
  119. package/src/util/utils.ts +51 -0
  120. package/src/zod.ts +1 -50
  121. package/dist/args-D5PNDyNu.mjs.map +0 -1
  122. package/dist/chunk-CjcI7cDX.mjs +0 -15
  123. package/dist/command-utils-B1D-HqCd.mjs +0 -1117
  124. package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
  125. package/dist/completion.d.mts +0 -64
  126. package/dist/completion.d.mts.map +0 -1
  127. package/dist/completion.mjs.map +0 -1
  128. package/dist/errors-BiVrBgi6.mjs.map +0 -1
  129. package/dist/formatter-DtHzbP22.d.mts.map +0 -1
  130. package/dist/help-bbmu9-qd.mjs.map +0 -1
  131. package/dist/mcp-mLWIdUIu.mjs.map +0 -1
  132. package/dist/serve-B0u43DK7.mjs.map +0 -1
  133. package/dist/stream-BcC146Ud.mjs.map +0 -1
  134. package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
  135. package/dist/update-check-CFX1FV3v.mjs.map +0 -1
  136. package/src/command-utils.ts +0 -882
  137. package/src/create.ts +0 -1829
  138. package/src/runtime.ts +0 -497
  139. package/src/types.ts +0 -1291
  140. package/src/utils.ts +0 -140
  141. /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
package/src/runtime.ts DELETED
@@ -1,497 +0,0 @@
1
- import type { ColorConfig, ColorTheme } from './colorizer.ts';
2
- import type { HelpFormat } from './formatter.ts';
3
- import { findConfigFile, loadConfigFile } from './utils.ts';
4
-
5
- /**
6
- * A progress indicator instance (spinner, progress bar, etc).
7
- * Created by the runtime's `progress` factory and used to show loading state during command execution.
8
- */
9
- export type PadroneProgressIndicator = {
10
- /** Update the displayed message. */
11
- update: (message: string) => void;
12
- /** Mark as succeeded and stop. Pass `null` to stop without rendering a final message. */
13
- succeed: (message?: string | null, options?: { indicator?: string }) => void;
14
- /** Mark as failed and stop. Pass `null` to stop without rendering a final message. */
15
- fail: (message?: string | null, options?: { indicator?: string }) => void;
16
- /** Stop without success/fail status. */
17
- stop: () => void;
18
- /** Temporarily hide the indicator so other output can be written cleanly. */
19
- pause: () => void;
20
- /** Redraw the indicator after a `pause()`. */
21
- resume: () => void;
22
- };
23
-
24
- /** Built-in spinner presets. */
25
- export type PadroneSpinnerPreset = 'dots' | 'line' | 'arc' | 'bounce';
26
-
27
- /**
28
- * Spinner configuration for progress indicators.
29
- * - A preset name (e.g., `'dots'`) to use built-in frames.
30
- * - An object with custom `frames` and/or `interval`.
31
- * - `false` to disable the spinner animation (static text only).
32
- */
33
- export type PadroneSpinnerConfig = PadroneSpinnerPreset | { frames?: string[]; interval?: number } | false;
34
-
35
- /**
36
- * Options passed to the runtime's `progress` factory.
37
- */
38
- export type PadroneProgressOptions = {
39
- spinner?: PadroneSpinnerConfig;
40
- /** Character/string shown before the success message. Defaults to `'✔'`. */
41
- successIndicator?: string;
42
- /** Character/string shown before the error message. Defaults to `'✖'`. */
43
- errorIndicator?: string;
44
- };
45
-
46
- /**
47
- * Controls interactive prompting capability and default behavior at the runtime level.
48
- * - `'supported'` — capable; caller decides.
49
- * - `'unsupported'` — hard veto; nothing can override.
50
- * - `'forced'` — capable and forces prompts by default.
51
- * - `'disabled'` — capable but suppresses prompts by default.
52
- */
53
- export type InteractiveMode = 'supported' | 'unsupported' | 'forced' | 'disabled';
54
-
55
- /**
56
- * Configuration passed to the runtime's `prompt` function for interactive field prompting.
57
- * The prompt type and choices are auto-detected from the field's JSON schema.
58
- */
59
- export type InteractivePromptConfig = {
60
- /** The field name being prompted. */
61
- name: string;
62
- /** Human-readable message/label for the prompt, derived from the field's description or name. */
63
- message: string;
64
- /** The prompt type, auto-detected from the JSON schema. */
65
- type: 'input' | 'confirm' | 'select' | 'multiselect' | 'password';
66
- /** Available choices for select/multiselect prompts. */
67
- choices?: { label: string; value: unknown }[];
68
- /** Default value from the schema. */
69
- default?: unknown;
70
- };
71
-
72
- /**
73
- * Defines the execution context for a Padrone program.
74
- * Abstracts all environment-dependent I/O so the CLI framework
75
- * can run outside of a terminal (e.g., web UIs, chat interfaces, testing).
76
- *
77
- * All fields are optional — unspecified fields fall back to the Node.js/Bun defaults.
78
- */
79
- export type PadroneRuntime = {
80
- /** Write normal output (replaces console.log). Receives the raw value — runtime handles formatting. */
81
- output?: (...args: unknown[]) => void;
82
- /** Write error output (replaces console.error). */
83
- error?: (text: string) => void;
84
- /** Return the raw CLI arguments (replaces process.argv.slice(2)). */
85
- argv?: () => string[];
86
- /** Return environment variables (replaces process.env). */
87
- env?: () => Record<string, string | undefined>;
88
- /** Default help output format. */
89
- format?: HelpFormat | 'auto';
90
- /** Color theme for ANSI/console help output. A theme name or partial color config. */
91
- theme?: ColorTheme | ColorConfig;
92
- /** Load and parse a config file by path. Return undefined if not found or unparsable. */
93
- loadConfigFile?: (path: string) => Record<string, unknown> | undefined;
94
- /** Find the first existing file from a list of candidate names. */
95
- findFile?: (names: string[]) => string | undefined;
96
- /**
97
- * Standard input abstraction. Provides methods to read piped data from stdin.
98
- * When not provided, defaults to reading from `process.stdin`.
99
- *
100
- * Used by commands that declare a `stdin` field in their arguments meta.
101
- * The framework reads stdin automatically during the validate phase and
102
- * injects the data into the specified argument field.
103
- */
104
- stdin?: {
105
- /** Whether stdin is a TTY (interactive terminal) vs a pipe/file. */
106
- isTTY?: boolean;
107
- /** Read all of stdin as a string. */
108
- text: () => Promise<string>;
109
- /** Async iterable of lines for streaming. */
110
- lines: () => AsyncIterable<string>;
111
- };
112
- /**
113
- * Controls interactive prompting capability and default behavior.
114
- * - `'supported'` — runtime can handle prompts; caller (flag/pref) decides whether to prompt. This is the default when `prompt` is provided.
115
- * - `'unsupported'` — runtime cannot handle prompts; hard veto that nothing can override.
116
- * - `'forced'` — runtime supports prompts and forces them by default (prompts even for provided values).
117
- * - `'disabled'` — runtime supports prompts but suppresses them by default.
118
- *
119
- * `'unsupported'` is the only immutable state. For the others, the `--interactive`/`-i` flag
120
- * and `cli()` preferences can override the default behavior.
121
- */
122
- interactive?: InteractiveMode;
123
- /**
124
- * Prompt the user for input. Called during `cli()` for fields marked as interactive.
125
- * When `interactive` is `true` and this is not provided, defaults to an Enquirer-based terminal prompt.
126
- */
127
- prompt?: (config: InteractivePromptConfig) => Promise<unknown>;
128
- /**
129
- * Create a progress indicator (spinner, progress bar, etc).
130
- * Used by commands that set `progress` in their config, or manually via `ctx.progress()` in actions.
131
- * When not provided, auto-progress is silently skipped and `ctx.progress()` returns a no-op indicator.
132
- */
133
- progress?: (message: string, options?: PadroneProgressOptions) => PadroneProgressIndicator;
134
- /**
135
- * Read a line of input from the user. Used by `repl()` for custom runtimes
136
- * (web UIs, chat interfaces, testing).
137
- * Returns the input string, `null` on EOF (e.g. Ctrl+D, closed connection),
138
- * or `REPL_SIGINT` when the user presses Ctrl+C.
139
- *
140
- * When not provided, `repl()` uses a built-in Node.js readline session
141
- * with command history (up/down arrows) and tab completion.
142
- */
143
- readLine?: (prompt: string) => Promise<string | typeof REPL_SIGINT | null>;
144
- };
145
-
146
- /**
147
- * Internal resolved runtime where all fields are guaranteed to be present.
148
- * The `prompt`, `interactive`, and `readLine` fields remain optional since not all runtimes provide them.
149
- */
150
- export type ResolvedPadroneRuntime = Required<
151
- Omit<PadroneRuntime, 'prompt' | 'interactive' | 'readLine' | 'stdin' | 'progress' | 'theme'>
152
- > &
153
- Pick<PadroneRuntime, 'prompt' | 'interactive' | 'readLine' | 'stdin' | 'progress' | 'theme'>;
154
-
155
- /**
156
- * Default terminal prompt implementation powered by Enquirer.
157
- * Lazily imported to avoid loading Enquirer when not needed.
158
- */
159
- async function defaultTerminalPrompt(config: InteractivePromptConfig): Promise<unknown> {
160
- const Enquirer = (await import('enquirer')).default;
161
-
162
- const question: Record<string, unknown> = {
163
- type: config.type,
164
- name: config.name,
165
- message: config.message,
166
- };
167
-
168
- if (config.default !== undefined) {
169
- question.initial = config.default;
170
- }
171
-
172
- if (config.choices) {
173
- question.choices = config.choices.map((c) => ({
174
- name: String(c.value),
175
- message: c.label,
176
- }));
177
- }
178
-
179
- const response = (await Enquirer.prompt(question as any)) as Record<string, unknown>;
180
- return response[config.name];
181
- }
182
-
183
- /**
184
- * Internal session config for the REPL's persistent readline interface.
185
- */
186
- export type ReplSessionConfig = {
187
- completer?: (line: string) => [string[], string];
188
- history?: string[];
189
- };
190
-
191
- /**
192
- * Creates a persistent Node.js readline session for the REPL.
193
- * Enables up/down arrow history navigation and tab completion.
194
- * Used internally by `repl()` when no custom `readLine` is provided.
195
- */
196
- /**
197
- * Sentinel value returned by the terminal REPL session when Ctrl+C is pressed.
198
- * Distinguished from empty string (user pressed enter) and null (EOF/Ctrl+D).
199
- */
200
- export const REPL_SIGINT = Symbol('REPL_SIGINT');
201
-
202
- export function createTerminalReplSession(config: ReplSessionConfig) {
203
- // History accumulates across per-call interfaces, giving us
204
- // up/down arrow navigation without a persistent stdin listener
205
- // that would conflict with Enquirer or other stdin consumers.
206
- let history: string[] = config.history ? [...config.history] : [];
207
- let currentCompleter = config.completer;
208
-
209
- return {
210
- /** Update the tab completer (e.g. when REPL scope changes). Takes effect on the next question. */
211
- set completer(fn: ((line: string) => [string[], string]) | undefined) {
212
- currentCompleter = fn;
213
- },
214
- async question(prompt: string): Promise<string | typeof REPL_SIGINT | null> {
215
- const { createInterface } = await import('node:readline');
216
- const opts: Record<string, unknown> = {
217
- input: process.stdin,
218
- output: process.stdout,
219
- terminal: true,
220
- history: [...history],
221
- historySize: Math.max(history.length, 1000),
222
- };
223
- if (currentCompleter) {
224
- opts.completer = currentCompleter;
225
- }
226
- const rl = createInterface(opts as any);
227
-
228
- return new Promise((resolve) => {
229
- let resolved = false;
230
- const settle = (value: string | typeof REPL_SIGINT | null) => {
231
- if (resolved) return;
232
- resolved = true;
233
- rl.close();
234
- resolve(value);
235
- };
236
-
237
- rl.question(prompt, (answer) => {
238
- // Grab updated history (includes the new entry) before closing.
239
- if (Array.isArray((rl as any).history)) history = [...(rl as any).history];
240
- settle(answer);
241
- });
242
- // Ctrl+C: cancel current line, print newline, resolve SIGINT sentinel.
243
- rl.once('SIGINT', () => {
244
- process.stdout.write('\n');
245
- settle(REPL_SIGINT);
246
- });
247
- // EOF (Ctrl+D) fires close without the question callback.
248
- rl.once('close', () => {
249
- // Write newline so zsh doesn't show '%' (partial-line indicator).
250
- process.stdout.write('\n');
251
- settle(null);
252
- });
253
- });
254
- },
255
- close() {
256
- // No persistent interface to clean up.
257
- },
258
- };
259
- }
260
-
261
- /**
262
- * Auto-detect interactive mode when not explicitly set.
263
- * Returns 'disabled' in CI environments or non-TTY contexts, 'supported' otherwise.
264
- */
265
- function detectInteractiveMode(): InteractiveMode {
266
- if (typeof process === 'undefined') return 'disabled';
267
- if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return 'disabled';
268
- if (!process.stdout?.isTTY) return 'disabled';
269
- return 'supported';
270
- }
271
-
272
- /**
273
- * Creates the default Node.js/Bun runtime.
274
- */
275
- /**
276
- * Creates a default stdin reader from `process.stdin`.
277
- * Only created when a command actually declares a `stdin` meta field.
278
- */
279
- function createDefaultStdin(): NonNullable<PadroneRuntime['stdin']> {
280
- return {
281
- get isTTY() {
282
- // process.stdin.isTTY is `true` when interactive terminal, `undefined` when piped/redirected.
283
- // Node.js never sets it to `false` — it's either `true` or absent.
284
- if (typeof process === 'undefined') return true;
285
- return process.stdin?.isTTY === true;
286
- },
287
- async text() {
288
- if (typeof process === 'undefined') return '';
289
- const chunks: Buffer[] = [];
290
- for await (const chunk of process.stdin) {
291
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
292
- }
293
- return Buffer.concat(chunks).toString('utf-8');
294
- },
295
- async *lines() {
296
- if (typeof process === 'undefined') return;
297
- const { createInterface } = await import('node:readline');
298
- const rl = createInterface({ input: process.stdin });
299
- try {
300
- for await (const line of rl) {
301
- yield line;
302
- }
303
- } finally {
304
- rl.close();
305
- }
306
- },
307
- };
308
- }
309
-
310
- const spinnerPresets: Record<PadroneSpinnerPreset, string[]> = {
311
- dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
312
- line: ['-', '\\', '|', '/'],
313
- arc: ['◜', '◠', '◝', '◞', '◡', '◟'],
314
- bounce: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'],
315
- };
316
-
317
- function resolveSpinnerConfig(config?: PadroneSpinnerConfig): { frames: string[]; interval: number; disabled: boolean } {
318
- if (config === false) return { frames: [], interval: 80, disabled: true };
319
- if (typeof config === 'string') return { frames: spinnerPresets[config], interval: 80, disabled: false };
320
- if (typeof config === 'object') {
321
- return {
322
- frames: config.frames ?? spinnerPresets.dots,
323
- interval: config.interval ?? 80,
324
- disabled: false,
325
- };
326
- }
327
- return { frames: spinnerPresets.dots, interval: 80, disabled: false };
328
- }
329
-
330
- /**
331
- * Creates a built-in terminal spinner. Returns a no-op indicator in non-TTY/CI environments.
332
- */
333
- function createTerminalSpinner(message: string, options?: PadroneProgressOptions): PadroneProgressIndicator {
334
- const { frames, interval, disabled: spinnerDisabled } = resolveSpinnerConfig(options?.spinner);
335
- const successIcon = options?.successIndicator ?? '✔';
336
- const errorIcon = options?.errorIndicator ?? '✖';
337
-
338
- const formatFinal = (icon: string, msg: string) => (icon ? `${icon} ${msg}\n` : `${msg}\n`);
339
-
340
- if (typeof process === 'undefined' || !process.stderr?.isTTY) {
341
- // Non-TTY: just log start/end, no animation
342
- return {
343
- update() {},
344
- succeed(msg, opts) {
345
- if (msg === null) return;
346
- const icon = opts?.indicator ?? successIcon;
347
- if (msg || message) process?.stderr?.write?.(formatFinal(icon, msg || message));
348
- },
349
- fail(msg, opts) {
350
- if (msg === null) return;
351
- const icon = opts?.indicator ?? errorIcon;
352
- if (msg || message) process?.stderr?.write?.(formatFinal(icon, msg || message));
353
- },
354
- stop() {},
355
- pause() {},
356
- resume() {},
357
- };
358
- }
359
-
360
- // If spinner is disabled and there's no message, nothing to render
361
- if (spinnerDisabled && !message) {
362
- return { update() {}, succeed() {}, fail() {}, stop() {}, pause() {}, resume() {} };
363
- }
364
-
365
- let frame = 0;
366
- let text = message;
367
- let stopped = false;
368
- let paused = false;
369
-
370
- const writeStderr = process.stderr.write.bind(process.stderr);
371
- const writeStdout = process.stdout.write.bind(process.stdout);
372
- const clearLine = () => writeStderr('\x1b[2K\r');
373
-
374
- const render = () => {
375
- if (paused || stopped) return;
376
- if (spinnerDisabled) {
377
- // Static text only, no spinner frames
378
- if (text) writeStderr(`\x1b[2K\r${text}`);
379
- } else {
380
- const prefix = frames[frame] ?? '';
381
- writeStderr(`\x1b[2K\r${text ? `${prefix} ${text}` : prefix}`);
382
- }
383
- };
384
-
385
- const timer = spinnerDisabled
386
- ? undefined
387
- : setInterval(() => {
388
- frame = (frame + 1) % frames.length;
389
- render();
390
- }, interval);
391
-
392
- render();
393
-
394
- const clear = () => {
395
- if (stopped) return;
396
- stopped = true;
397
- paused = false;
398
- if (timer) clearInterval(timer);
399
- clearLine();
400
- };
401
-
402
- return {
403
- update(msg) {
404
- if (stopped) return;
405
- text = msg;
406
- render();
407
- },
408
- succeed(msg, opts) {
409
- clear();
410
- if (msg === null) return;
411
- const finalMsg = msg ?? text;
412
- const icon = opts?.indicator ?? successIcon;
413
- if (finalMsg) writeStderr(formatFinal(icon, finalMsg));
414
- },
415
- fail(msg, opts) {
416
- clear();
417
- if (msg === null) return;
418
- const finalMsg = msg ?? text;
419
- const icon = opts?.indicator ?? errorIcon;
420
- if (finalMsg) writeStderr(formatFinal(icon, finalMsg));
421
- },
422
- stop() {
423
- clear();
424
- },
425
- pause() {
426
- if (stopped || paused) return;
427
- paused = true;
428
- clearLine();
429
- writeStdout('\x1b[2K\r');
430
- },
431
- resume() {
432
- if (stopped || !paused) return;
433
- paused = false;
434
- render();
435
- },
436
- };
437
- }
438
-
439
- export function createDefaultRuntime(): ResolvedPadroneRuntime {
440
- return {
441
- output: (...args) => console.log(...args),
442
- error: (text) => console.error(text),
443
- argv: () => (typeof process !== 'undefined' ? process.argv.slice(2) : []),
444
- env: () => (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : {}),
445
- format: 'auto',
446
- loadConfigFile,
447
- findFile: findConfigFile,
448
- prompt: defaultTerminalPrompt,
449
- interactive: detectInteractiveMode(),
450
- progress: createTerminalSpinner,
451
- };
452
- }
453
-
454
- /**
455
- * Merges a partial runtime with the default runtime.
456
- */
457
- /**
458
- * Returns the stdin abstraction: custom runtime stdin > default process.stdin.
459
- * Returns `undefined` when no custom stdin is provided and process.stdin is not piped.
460
- */
461
- export function resolveStdin(partial?: PadroneRuntime): NonNullable<PadroneRuntime['stdin']> | undefined {
462
- if (partial?.stdin) return partial.stdin;
463
- const defaultStdin = createDefaultStdin();
464
- // Only use default stdin if it's actually piped (isTTY === false).
465
- // This avoids accidentally blocking on stdin in tests/CI.
466
- if (defaultStdin.isTTY) return undefined;
467
- return defaultStdin;
468
- }
469
-
470
- /**
471
- * Like `resolveStdin`, but always returns a stdin source even when it's a TTY.
472
- * Used for async streams which support interactive (non-piped) input.
473
- */
474
- export function resolveStdinAlways(partial?: PadroneRuntime): NonNullable<PadroneRuntime['stdin']> {
475
- if (partial?.stdin) return partial.stdin;
476
- return createDefaultStdin();
477
- }
478
-
479
- export function resolveRuntime(partial?: PadroneRuntime): ResolvedPadroneRuntime {
480
- const defaults = createDefaultRuntime();
481
- if (!partial) return defaults;
482
- return {
483
- output: partial.output ?? defaults.output,
484
- error: partial.error ?? defaults.error,
485
- argv: partial.argv ?? defaults.argv,
486
- env: partial.env ?? defaults.env,
487
- format: partial.format ?? defaults.format,
488
- loadConfigFile: partial.loadConfigFile ?? defaults.loadConfigFile,
489
- findFile: partial.findFile ?? defaults.findFile,
490
- interactive: partial.interactive ?? defaults.interactive,
491
- prompt: partial.prompt ?? defaults.prompt,
492
- readLine: partial.readLine ?? defaults.readLine,
493
- progress: partial.progress ?? defaults.progress,
494
- stdin: partial.stdin,
495
- theme: partial.theme,
496
- };
497
- }