padrone 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
@@ -0,0 +1,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
+ }
@@ -0,0 +1,94 @@
1
+ import { defineInterceptor } from '../core/interceptors.ts';
2
+ import { parseCliInputToParts } from '../core/parse.ts';
3
+ import { withDrain } from '../core/results.ts';
4
+ import type {
5
+ AnyPadroneBuilder,
6
+ AnyPadroneCommand,
7
+ CommandTypesBase,
8
+ InterceptorStartContext,
9
+ PadroneCommand,
10
+ PadroneReplPreferences,
11
+ } from '../types/index.ts';
12
+ import type { PadroneSchema } from '../types/schema.ts';
13
+ import type { WithCommand } from '../util/type-utils.ts';
14
+ import { passthroughSchema } from './utils.ts';
15
+
16
+ // ── Types ────────────────────────────────────────────────────────────────
17
+
18
+ type ReplArgs = { scope?: string };
19
+
20
+ type ReplCommand = PadroneCommand<'repl', '', PadroneSchema<ReplArgs>, void, [], [], true>;
21
+
22
+ export type WithRepl<T> = WithCommand<T, 'repl', ReplCommand>;
23
+
24
+ // ── Extension ────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Extension that adds REPL support:
28
+ * - `repl` command that starts an interactive REPL
29
+ * - `--repl` flag that starts the REPL from any invocation
30
+ *
31
+ * Usage:
32
+ * ```ts
33
+ * createPadrone('my-cli').extend(padroneRepl())
34
+ * ```
35
+ */
36
+ export function padroneRepl(
37
+ defaults?: PadroneReplPreferences & { disabled?: boolean },
38
+ ): <T extends CommandTypesBase>(builder: T) => WithRepl<T> {
39
+ const disabled = defaults?.disabled;
40
+ return ((builder: AnyPadroneBuilder) =>
41
+ builder
42
+ .command('repl', (c) =>
43
+ c
44
+ .configure({ description: 'Start an interactive REPL', hidden: true })
45
+ .arguments(passthroughSchema({ scope: 'string' }), { positional: ['scope'] })
46
+ .async()
47
+ .action(async (args, ctx) => {
48
+ const prefs: PadroneReplPreferences = { ...defaults, scope: args.scope ?? defaults?.scope };
49
+ const repl = ctx.program.repl(prefs);
50
+ const { value } = await repl.drain();
51
+ return value;
52
+ }),
53
+ )
54
+ .intercept(createReplInterceptor(defaults, disabled))) as any;
55
+ }
56
+
57
+ function createReplInterceptor(defaults?: PadroneReplPreferences, disabled?: boolean) {
58
+ return defineInterceptor({ id: 'padrone:repl', name: 'padrone:repl', order: -1000, disabled }, () => ({
59
+ start(ctx: InterceptorStartContext, next: () => unknown) {
60
+ const replInfo = checkReplFlag(ctx.input, ctx.command);
61
+ if (!replInfo) return next();
62
+
63
+ const program = ctx.program;
64
+ if (!program?.repl) return next();
65
+
66
+ const prefs: PadroneReplPreferences = { ...defaults, scope: replInfo.scope ?? defaults?.scope };
67
+
68
+ // Return a Promise so the pipeline awaits the REPL result
69
+ return program
70
+ .repl(prefs)
71
+ .drain()
72
+ .then((r: any) => withDrain({ command: ctx.command, args: undefined, result: r.value }));
73
+ },
74
+ }));
75
+ }
76
+
77
+ /** Check for --repl flag in input. */
78
+ function checkReplFlag(input: string | undefined, rootCommand: AnyPadroneCommand): { scope?: string } | null {
79
+ if (!input) return null;
80
+
81
+ const parts = parseCliInputToParts(input);
82
+ const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
83
+ const args = parts.filter((p) => p.type === 'named');
84
+ const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name;
85
+
86
+ const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl'));
87
+ if (!hasReplFlag) return null;
88
+
89
+ const normalizedTerms = [...terms];
90
+ if (normalizedTerms[0] === rootCommand.name) normalizedTerms.shift();
91
+
92
+ const scope = normalizedTerms.length > 0 ? normalizedTerms.join(' ') : undefined;
93
+ return { scope };
94
+ }
@@ -0,0 +1,48 @@
1
+ import { resolveAllCommands } from '../core/commands.ts';
2
+ import type { PadroneServePreferences } from '../feature/serve.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 ServeArgs = { port?: string; host?: string; basePath?: string };
12
+
13
+ type ServeCommand = PadroneCommand<'serve', '', PadroneSchema<ServeArgs>, void, [], [], true>;
14
+
15
+ export type WithServe<T> = WithCommand<T, 'serve', ServeCommand>;
16
+
17
+ // ── Extension ────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Extension that adds the `serve` command for starting a REST HTTP server.
21
+ *
22
+ * Usage:
23
+ * ```ts
24
+ * createPadrone('my-cli').extend(padroneServe())
25
+ * ```
26
+ */
27
+ export function padroneServe(defaults?: PadroneServePreferences): <T extends CommandTypesBase>(builder: T) => WithServe<T> {
28
+ return ((builder: AnyPadroneBuilder) =>
29
+ builder.command('serve', (c) =>
30
+ c
31
+ .configure({ description: 'Start a REST HTTP server', hidden: true })
32
+ .arguments(passthroughSchema({ port: 'string', host: 'string', 'base-path': 'string' }))
33
+ .async()
34
+ .action(async (args, ctx) => {
35
+ const rootCommand = getRootCommand(ctx.command);
36
+ resolveAllCommands(rootCommand);
37
+ const { startServeServer } = await import('../feature/serve.ts');
38
+ const port = args.port ? parseInt(args.port, 10) : undefined;
39
+ const prefs: PadroneServePreferences = {
40
+ ...defaults,
41
+ port: port && !Number.isNaN(port) ? port : defaults?.port,
42
+ host: args.host ?? defaults?.host,
43
+ basePath: args['base-path'] ?? defaults?.basePath,
44
+ };
45
+ await startServeServer(ctx.program, rootCommand, ctx.program.eval, prefs);
46
+ }),
47
+ )) as any;
48
+ }
@@ -0,0 +1,87 @@
1
+ import { SignalError, signalExitCode } from '../core/errors.ts';
2
+ import { defineInterceptor } from '../core/interceptors.ts';
3
+ import { thenMaybe } from '../core/results.ts';
4
+ import type { PadroneSignal } from '../core/runtime.ts';
5
+ import type { AnyPadroneBuilder, CommandTypesBase } from '../types/index.ts';
6
+
7
+ // ── Interceptor ─────────────────────────────────────────────────────────
8
+
9
+ const signalMeta = { id: 'padrone:signal', name: 'padrone:signal', order: -2000 } as const;
10
+
11
+ const signalInterceptor = defineInterceptor(signalMeta, () => {
12
+ const abortController = new AbortController();
13
+ let receivedSignal: PadroneSignal | undefined;
14
+ let lastSigintTime = 0;
15
+ let unsubscribe: (() => void) | undefined;
16
+ const DOUBLE_SIGINT_MS = 2000;
17
+
18
+ const cleanup = () => {
19
+ unsubscribe?.();
20
+ unsubscribe = undefined;
21
+ };
22
+
23
+ const attachSignalInfo = <T>(result: T): T => {
24
+ if (receivedSignal && result && typeof result === 'object') {
25
+ (result as any).signal = receivedSignal;
26
+ (result as any).exitCode = signalExitCode(receivedSignal);
27
+ }
28
+ return result;
29
+ };
30
+
31
+ return {
32
+ start(ctx, next) {
33
+ const runtimeExit = ctx.runtime.exit;
34
+ unsubscribe = ctx.runtime.onSignal?.((sig) => {
35
+ if (abortController.signal.aborted) {
36
+ if (sig === 'SIGINT') {
37
+ const elapsed = Date.now() - lastSigintTime;
38
+ if (elapsed > 0 && elapsed < DOUBLE_SIGINT_MS) {
39
+ runtimeExit?.(signalExitCode(sig));
40
+ }
41
+ lastSigintTime = Date.now();
42
+ }
43
+ return;
44
+ }
45
+ if (sig === 'SIGINT') lastSigintTime = Date.now();
46
+ receivedSignal = sig;
47
+ abortController.abort(sig);
48
+ });
49
+
50
+ const result = next({ signal: abortController.signal });
51
+ return thenMaybe(result, (r) => {
52
+ cleanup();
53
+ return attachSignalInfo(r);
54
+ });
55
+ },
56
+ error(_ctx, next) {
57
+ return thenMaybe(next(), (er) => {
58
+ if (receivedSignal && er.error instanceof Error) {
59
+ er.error = new SignalError(receivedSignal, { cause: er.error });
60
+ }
61
+ return er;
62
+ });
63
+ },
64
+ shutdown(_ctx, next) {
65
+ cleanup();
66
+ return next();
67
+ },
68
+ };
69
+ });
70
+
71
+ // ── Extension ───────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Extension that wires process signal handling (SIGINT, SIGTERM, SIGHUP) into the interceptor lifecycle.
75
+ *
76
+ * - Creates an `AbortController` whose signal is propagated to all downstream phases.
77
+ * - Subscribes to `runtime.onSignal` to forward OS signals to the abort controller.
78
+ * - Implements SIGINT double-tap: two SIGINTs within 2 seconds force-exits the process.
79
+ * - Attaches `signal` and `exitCode` to results and errors when interrupted.
80
+ * - Cleans up the signal subscription on completion or failure.
81
+ *
82
+ * Included in the default extensions. Runs at order `-2000` (outermost).
83
+ */
84
+ export function padroneSignalHandling(options?: { disabled?: boolean }): <T extends CommandTypesBase>(builder: T) => T {
85
+ const interceptor = options?.disabled ? defineInterceptor({ ...signalMeta, disabled: true }, () => ({})) : signalInterceptor;
86
+ return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
87
+ }
@@ -0,0 +1,62 @@
1
+ import { applyValues, isArrayField, isAsyncStreamField } from '../core/args.ts';
2
+ import { resolveStdin, resolveStdinAlways } from '../core/default-runtime.ts';
3
+ import { defineInterceptor } from '../core/interceptors.ts';
4
+ import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';
5
+ import { createStdinStream } from '../util/stream.ts';
6
+
7
+ // ── Interceptor ─────────────────────────────────────────────────────────
8
+
9
+ const stdinInterceptor = defineInterceptor({ id: 'padrone:stdin', name: 'padrone:stdin', order: -1001 }, () => ({
10
+ validate(ctx: InterceptorValidateContext, next) {
11
+ const stdinField = ctx.command.meta?.stdin;
12
+ if (!stdinField) return next();
13
+
14
+ // Skip if the field was already provided via CLI flags
15
+ if (stdinField in ctx.rawArgs && ctx.rawArgs[stdinField] !== undefined) return next();
16
+
17
+ const streamInfo = isAsyncStreamField(ctx.command.argsSchema, stdinField);
18
+ if (streamInfo) {
19
+ const stdinForStream = resolveStdinAlways(ctx.runtime as any);
20
+ const mergedRawArgs = applyValues(ctx.rawArgs, { [stdinField]: createStdinStream(stdinForStream, streamInfo.itemSchema) });
21
+ return next({ rawArgs: mergedRawArgs });
22
+ }
23
+
24
+ const stdin = resolveStdin(ctx.runtime as any);
25
+ if (!stdin) return next();
26
+
27
+ if (isArrayField(ctx.command.argsSchema, stdinField)) {
28
+ return (async () => {
29
+ const lines: string[] = [];
30
+ for await (const line of stdin.lines()) {
31
+ lines.push(line);
32
+ }
33
+ const mergedRawArgs = applyValues(ctx.rawArgs, { [stdinField]: lines });
34
+ return next({ rawArgs: mergedRawArgs });
35
+ })();
36
+ }
37
+
38
+ return stdin.text().then((text) => {
39
+ if (!text) return next();
40
+ const mergedRawArgs = applyValues(ctx.rawArgs, { [stdinField]: text });
41
+ return next({ rawArgs: mergedRawArgs });
42
+ });
43
+ },
44
+ }));
45
+
46
+ // ── Extension ────────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Extension that reads stdin data into the argument field specified by `meta.stdin`.
50
+ * Included by default via `createPadrone()`.
51
+ *
52
+ * Read mode is inferred from the schema type:
53
+ * - `string` field → reads all stdin as a single string
54
+ * - `string[]` field → reads stdin line-by-line into an array
55
+ * - `AsyncIterable` field → returns a stream for line-by-line async consumption
56
+ *
57
+ * Stdin is only read when piped (not a TTY) and the field wasn't already provided via CLI flags.
58
+ */
59
+ export function padroneStdin(options?: { disabled?: boolean }): <T extends CommandTypesBase>(builder: T) => T {
60
+ const interceptor = options?.disabled ? defineInterceptor({ ...stdinInterceptor, disabled: true }, () => ({})) : stdinInterceptor;
61
+ return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
62
+ }