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,246 @@
1
+ import type { ColorConfig, ColorTheme } from '../output/colorizer.ts';
2
+ import type { HelpFormat } from '../output/formatter.ts';
3
+
4
+ /** Process signals that Padrone can handle for graceful shutdown. */
5
+ export type PadroneSignal = 'SIGINT' | 'SIGTERM' | 'SIGHUP';
6
+
7
+ /** Value accepted by `PadroneProgressIndicator.update()`. */
8
+ export type PadroneProgressUpdate = string | number | { message?: string; progress?: number; indeterminate?: boolean; time?: boolean };
9
+
10
+ /**
11
+ * A progress indicator instance (spinner, progress bar, etc).
12
+ * Created by the runtime's `progress` factory and used to show loading state during command execution.
13
+ */
14
+ export type PadroneProgressIndicator = {
15
+ /**
16
+ * Update the indicator.
17
+ * - `string` — update the displayed message.
18
+ * - `number` — set progress ratio (0–1). Values outside this range are clamped.
19
+ * - `{ message?, progress?, indeterminate? }` — update message, progress, or both.
20
+ *
21
+ * Set `indeterminate: true` to force the bar into indeterminate mode (shows animation, no percentage).
22
+ * This makes the bar visible even in `show: 'auto'` mode without providing a number.
23
+ * Omitting `progress` (or passing a string) leaves the bar in its current state.
24
+ * Setting `progress` when bar mode is not enabled is a no-op for the bar portion.
25
+ *
26
+ * Set `time: true` to start the elapsed timer on demand (useful when `time` was not set in options).
27
+ * Set `time: false` to hide the elapsed timer.
28
+ */
29
+ update: (value: PadroneProgressUpdate) => void;
30
+ /** Mark as succeeded and stop. Pass `null` to stop without rendering a final message. */
31
+ succeed: (message?: string | null, options?: { indicator?: string }) => void;
32
+ /** Mark as failed and stop. Pass `null` to stop without rendering a final message. */
33
+ fail: (message?: string | null, options?: { indicator?: string }) => void;
34
+ /** Stop without success/fail status. */
35
+ stop: () => void;
36
+ /** Temporarily hide the indicator so other output can be written cleanly. */
37
+ pause: () => void;
38
+ /** Redraw the indicator after a `pause()`. */
39
+ resume: () => void;
40
+ };
41
+
42
+ /** Controls when a progress element (spinner or bar) is visible. */
43
+ export type PadroneProgressShow = 'auto' | 'always' | 'never';
44
+
45
+ /** Built-in spinner presets. */
46
+ export type PadroneSpinnerPreset = 'dots' | 'line' | 'arc' | 'bounce';
47
+
48
+ /**
49
+ * Spinner configuration for progress indicators.
50
+ * - A preset name (e.g., `'dots'`) to use built-in frames.
51
+ * - `true` — default spinner with `show: 'always'` (visible even alongside a bar).
52
+ * - An object with custom `frames`, `interval`, and/or `show`.
53
+ * - `false` to disable the spinner (`show: 'never'`).
54
+ *
55
+ * Default `show` is `'auto'`: visible when the bar is not shown.
56
+ */
57
+ export type PadroneSpinnerConfig = PadroneSpinnerPreset | boolean | { frames?: string[]; interval?: number; show?: PadroneProgressShow };
58
+
59
+ /**
60
+ * Options passed to the runtime's `progress` factory.
61
+ */
62
+ /** Common fill/empty character pairs for progress bars. */
63
+ export type PadroneBarChar = '█' | '░' | '▓' | '▒' | '─' | '━' | '■' | '□' | '#' | '-' | '=' | '·' | '▰' | '▱' | (string & {});
64
+
65
+ /**
66
+ * Built-in indeterminate bar animation presets.
67
+ * - `'bounce'` — a filled segment slides back and forth (default).
68
+ * - `'slide'` — a filled segment slides left-to-right and wraps around.
69
+ * - `'pulse'` — the entire bar fades in and out using gradient characters (`░▒▓█▓▒░`).
70
+ */
71
+ export type PadroneBarAnimation = 'bounce' | 'slide' | 'pulse';
72
+
73
+ /**
74
+ * Progress bar configuration.
75
+ */
76
+ export type PadroneBarConfig = {
77
+ /** Total width of the bar in characters. Defaults to `20`. */
78
+ width?: number;
79
+ /** Character used for the filled portion of the bar. Defaults to `'█'`. */
80
+ filled?: PadroneBarChar;
81
+ /** Character used for the empty portion of the bar. Defaults to `'░'`. */
82
+ empty?: PadroneBarChar;
83
+ /** Indeterminate animation style. Defaults to `'bounce'`. */
84
+ animation?: PadroneBarAnimation;
85
+ /**
86
+ * When the bar is visible. Defaults to `'always'` when bar is enabled, `'auto'` when bar is not explicitly configured.
87
+ * - `'always'` — bar is always shown (indeterminate until a number is provided).
88
+ * - `'auto'` — bar is shown only after `update()` is called with a number.
89
+ * - `'never'` — bar is never shown.
90
+ */
91
+ show?: PadroneProgressShow;
92
+ };
93
+
94
+ export type PadroneProgressOptions = {
95
+ spinner?: PadroneSpinnerConfig;
96
+ /** Enable a progress bar. `true` for defaults (`show: 'always'`), or a `PadroneBarConfig` object. `false` to disable entirely. When omitted, bar defaults to `show: 'auto'` (appears when a number is provided). */
97
+ bar?: boolean | PadroneBarConfig;
98
+ /** Show elapsed time since the indicator started. Defaults to `false`. */
99
+ time?: boolean;
100
+ /** Show estimated time remaining based on progress rate. Requires numeric `update()` calls. Defaults to `false`. */
101
+ eta?: boolean;
102
+ /** Character/string shown before the success message. Defaults to `'✔'`. */
103
+ successIndicator?: string;
104
+ /** Character/string shown before the error message. Defaults to `'✖'`. */
105
+ errorIndicator?: string;
106
+ };
107
+
108
+ /**
109
+ * Controls interactive prompting capability and default behavior at the runtime level.
110
+ * - `'supported'` — capable; caller decides.
111
+ * - `'unsupported'` — hard veto; nothing can override.
112
+ * - `'forced'` — capable and forces prompts by default.
113
+ * - `'disabled'` — capable but suppresses prompts by default.
114
+ */
115
+ export type InteractiveMode = 'supported' | 'unsupported' | 'forced' | 'disabled';
116
+
117
+ /**
118
+ * Configuration passed to the runtime's `prompt` function for interactive field prompting.
119
+ * The prompt type and choices are auto-detected from the field's JSON schema.
120
+ */
121
+ export type InteractivePromptConfig = {
122
+ /** The field name being prompted. */
123
+ name: string;
124
+ /** Human-readable message/label for the prompt, derived from the field's description or name. */
125
+ message: string;
126
+ /** The prompt type, auto-detected from the JSON schema. */
127
+ type: 'input' | 'confirm' | 'select' | 'multiselect' | 'password';
128
+ /** Available choices for select/multiselect prompts. */
129
+ choices?: { label: string; value: unknown }[];
130
+ /** Default value from the schema. */
131
+ default?: unknown;
132
+ };
133
+
134
+ /**
135
+ * Defines the execution context for a Padrone program.
136
+ * Abstracts all environment-dependent I/O so the CLI framework
137
+ * can run outside of a terminal (e.g., web UIs, chat interfaces, testing).
138
+ *
139
+ * All fields are optional — unspecified fields fall back to the Node.js/Bun defaults.
140
+ */
141
+ export type PadroneRuntime = {
142
+ /** Write normal output (replaces console.log). Receives the raw value — runtime handles formatting. */
143
+ output?: (...args: unknown[]) => void;
144
+ /** Write error output (replaces console.error). */
145
+ error?: (text: string) => void;
146
+ /** Return the raw CLI arguments (replaces process.argv.slice(2)). */
147
+ argv?: () => string[];
148
+ /** Return environment variables (replaces process.env). */
149
+ env?: () => Record<string, string | undefined>;
150
+ /** Default help output format. */
151
+ format?: HelpFormat | 'auto';
152
+ /** Color theme for ANSI/console help output. A theme name or partial color config. */
153
+ theme?: ColorTheme | ColorConfig;
154
+ /**
155
+ * Standard input abstraction. Provides methods to read piped data from stdin.
156
+ * When not provided, defaults to reading from `process.stdin`.
157
+ *
158
+ * Used by commands that declare a `stdin` field in their arguments meta.
159
+ * The framework reads stdin automatically during the validate phase and
160
+ * injects the data into the specified argument field.
161
+ */
162
+ stdin?: {
163
+ /** Whether stdin is a TTY (interactive terminal) vs a pipe/file. */
164
+ isTTY?: boolean;
165
+ /** Read all of stdin as a string. */
166
+ text: () => Promise<string>;
167
+ /** Async iterable of lines for streaming. */
168
+ lines: () => AsyncIterable<string>;
169
+ };
170
+ /**
171
+ * Controls interactive prompting capability and default behavior.
172
+ * - `'supported'` — runtime can handle prompts; caller (flag/pref) decides whether to prompt. This is the default when `prompt` is provided.
173
+ * - `'unsupported'` — runtime cannot handle prompts; hard veto that nothing can override.
174
+ * - `'forced'` — runtime supports prompts and forces them by default (prompts even for provided values).
175
+ * - `'disabled'` — runtime supports prompts but suppresses them by default.
176
+ *
177
+ * `'unsupported'` is the only immutable state. For the others, the `--interactive`/`-i` flag
178
+ * and `cli()` preferences can override the default behavior.
179
+ */
180
+ interactive?: InteractiveMode;
181
+ /**
182
+ * Prompt the user for input. Called during `cli()` for fields marked as interactive.
183
+ * When `interactive` is `true` and this is not provided, defaults to an Enquirer-based terminal prompt.
184
+ */
185
+ prompt?: (config: InteractivePromptConfig) => Promise<unknown>;
186
+ /**
187
+ * Read a line of input from the user. Used by `repl()` for custom runtimes
188
+ * (web UIs, chat interfaces, testing).
189
+ * Returns the input string, `null` on EOF (e.g. Ctrl+D, closed connection),
190
+ * or `REPL_SIGINT` when the user presses Ctrl+C.
191
+ *
192
+ * When not provided, `repl()` uses a built-in Node.js readline session
193
+ * with command history (up/down arrows) and tab completion.
194
+ */
195
+ readLine?: (prompt: string) => Promise<string | typeof REPL_SIGINT | null>;
196
+
197
+ /**
198
+ * Register a callback for process signals. Returns an unsubscribe function.
199
+ * The default runtime wires this to `process.on('SIGINT' | 'SIGTERM' | 'SIGHUP')`.
200
+ * Non-Node runtimes (web UIs, tests) can map their own cancellation semantics.
201
+ *
202
+ * When not provided, signal handling is disabled for this runtime.
203
+ */
204
+ onSignal?: (callback: (signal: PadroneSignal) => void) => () => void;
205
+
206
+ /**
207
+ * Terminal/output capabilities. Used for ANSI detection, text wrapping, and TTY checks.
208
+ * The default runtime auto-detects from `process.stdout`. Non-terminal runtimes
209
+ * (web UIs, tests) should provide explicit values.
210
+ */
211
+ terminal?: {
212
+ /** Number of columns in the terminal. Used for text wrapping. */
213
+ columns?: number;
214
+ /** Whether stdout is a TTY. Affects ANSI color output and interactive features. */
215
+ isTTY?: boolean;
216
+ };
217
+
218
+ /**
219
+ * Force-exit the process. The default runtime wires this to `process.exit()`.
220
+ * Non-Node runtimes can throw an error or no-op.
221
+ */
222
+ exit?: (code: number) => never;
223
+ };
224
+
225
+ /**
226
+ * Internal resolved runtime where all fields are guaranteed to be present.
227
+ * The `prompt`, `interactive`, and `readLine` fields remain optional since not all runtimes provide them.
228
+ */
229
+ export type ResolvedPadroneRuntime = Required<
230
+ Omit<PadroneRuntime, 'prompt' | 'interactive' | 'readLine' | 'stdin' | 'theme' | 'onSignal' | 'terminal' | 'exit'>
231
+ > &
232
+ Pick<PadroneRuntime, 'prompt' | 'interactive' | 'readLine' | 'stdin' | 'theme' | 'onSignal' | 'terminal' | 'exit'>;
233
+
234
+ /**
235
+ * Sentinel value returned by the terminal REPL session when Ctrl+C is pressed.
236
+ * Distinguished from empty string (user pressed enter) and null (EOF/Ctrl+D).
237
+ */
238
+ export const REPL_SIGINT = Symbol('REPL_SIGINT');
239
+
240
+ /**
241
+ * Internal session config for the REPL's persistent readline interface.
242
+ */
243
+ export type ReplSessionConfig = {
244
+ completer?: (line: string) => [string[], string];
245
+ history?: string[];
246
+ };
@@ -0,0 +1,247 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { AnyPadroneCommand, InterceptorValidateResult } from '../types/index.ts';
3
+ import { coerceArgs, detectUnknownArgs, extractSchemaMetadata, getJsonSchema, parsePositionalConfig, preprocessArgs } from './args.ts';
4
+ import { getCommandRuntime } from './commands.ts';
5
+ import { getNestedValue, parseCliInputToParts, setNestedValue } from './parse.ts';
6
+ import { thenMaybe } from './results.ts';
7
+
8
+ /**
9
+ * Parses CLI input to find the command and extract raw arguments without validation.
10
+ */
11
+ export function parseCommand(input: string | undefined, rootCommand: AnyPadroneCommand, findCommandByName: FindCommandFn) {
12
+ input ??= getCommandRuntime(rootCommand).argv().join(' ') || undefined;
13
+ if (!input) {
14
+ const defaultCommand = findCommandByName('', rootCommand.commands);
15
+ if (defaultCommand) {
16
+ return { command: defaultCommand, rawArgs: {} as Record<string, unknown>, args: [] as string[], unmatchedTerms: [] as string[] };
17
+ }
18
+ return { command: rootCommand, rawArgs: {} as Record<string, unknown>, args: [] as string[], unmatchedTerms: [] as string[] };
19
+ }
20
+
21
+ const parts = parseCliInputToParts(input);
22
+
23
+ const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
24
+ const argTokens = parts.filter((p) => p.type === 'arg').map((p) => p.value);
25
+
26
+ let curCommand: AnyPadroneCommand | undefined = rootCommand;
27
+ let unmatchedTerms: string[] = [];
28
+
29
+ if (terms[0] === rootCommand.name) terms.shift();
30
+
31
+ for (let i = 0; i < terms.length; i++) {
32
+ const term = terms[i] || '';
33
+ const found = findCommandByName(term, curCommand.commands);
34
+
35
+ if (found) {
36
+ curCommand = found;
37
+ } else {
38
+ unmatchedTerms = terms.slice(i);
39
+ argTokens.unshift(...unmatchedTerms);
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (unmatchedTerms.length === 0 && curCommand.commands?.length) {
45
+ const defaultCommand = findCommandByName('', curCommand.commands);
46
+ if (defaultCommand) curCommand = defaultCommand;
47
+ }
48
+
49
+ if (!curCommand) return { command: rootCommand, rawArgs: {} as Record<string, unknown>, args: argTokens, unmatchedTerms };
50
+
51
+ const argsMeta = curCommand.meta?.fields;
52
+ const schemaMetadata = curCommand.argsSchema
53
+ ? extractSchemaMetadata(curCommand.argsSchema, argsMeta, curCommand.meta?.autoAlias)
54
+ : { flags: {}, aliases: {} };
55
+ const { flags, aliases } = schemaMetadata;
56
+
57
+ const arrayArguments = new Set<string>();
58
+ if (curCommand.argsSchema) {
59
+ try {
60
+ const jsonSchema = getJsonSchema(curCommand.argsSchema) as Record<string, any>;
61
+ if (jsonSchema.type === 'object' && jsonSchema.properties) {
62
+ for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
63
+ if (prop?.type === 'array') arrayArguments.add(key);
64
+ }
65
+ }
66
+ } catch {
67
+ // Ignore schema parsing errors
68
+ }
69
+ }
70
+
71
+ const argParts = parts.filter((p) => p.type === 'named' || p.type === 'alias');
72
+ const rawArgs: Record<string, unknown> = {};
73
+
74
+ for (const arg of argParts) {
75
+ let key: string[];
76
+ if (arg.type === 'alias' && arg.key.length === 1 && flags[arg.key[0]!]) {
77
+ key = [flags[arg.key[0]!]!];
78
+ } else if (arg.type === 'named' && arg.key.length === 1 && aliases[arg.key[0]!]) {
79
+ key = [aliases[arg.key[0]!]!];
80
+ } else {
81
+ key = arg.key;
82
+ }
83
+
84
+ const rootKey = key[0]!;
85
+
86
+ if (arg.type === 'named' && arg.negated) {
87
+ setNestedValue(rawArgs, key, false);
88
+ continue;
89
+ }
90
+
91
+ const value = arg.value ?? true;
92
+
93
+ if (arrayArguments.has(rootKey)) {
94
+ const existing = getNestedValue(rawArgs, key);
95
+ if (existing !== undefined) {
96
+ if (Array.isArray(existing)) {
97
+ if (Array.isArray(value)) existing.push(...value);
98
+ else existing.push(value);
99
+ } else {
100
+ if (Array.isArray(value)) setNestedValue(rawArgs, key, [existing, ...value]);
101
+ else setNestedValue(rawArgs, key, [existing, value]);
102
+ }
103
+ } else {
104
+ setNestedValue(rawArgs, key, Array.isArray(value) ? value : [value]);
105
+ }
106
+ } else {
107
+ const existing = getNestedValue(rawArgs, key);
108
+ if (existing !== undefined) {
109
+ if (Array.isArray(existing)) {
110
+ if (Array.isArray(value)) existing.push(...value);
111
+ else existing.push(value);
112
+ } else {
113
+ if (Array.isArray(value)) setNestedValue(rawArgs, key, [existing, ...value]);
114
+ else setNestedValue(rawArgs, key, [existing, value]);
115
+ }
116
+ } else {
117
+ setNestedValue(rawArgs, key, value);
118
+ }
119
+ }
120
+ }
121
+
122
+ return { command: curCommand, rawArgs, args: argTokens, unmatchedTerms };
123
+ }
124
+
125
+ type FindCommandFn = (name: string, commands?: AnyPadroneCommand[]) => AnyPadroneCommand | undefined;
126
+
127
+ /**
128
+ * Preprocesses raw arguments: maps positional arguments and performs auto-coercion.
129
+ * External data sources (stdin, env, config) are handled by extensions before this runs.
130
+ */
131
+ export function buildCommandArgs(
132
+ command: AnyPadroneCommand,
133
+ rawArgs: Record<string, unknown>,
134
+ positionalArgs: string[],
135
+ ): Record<string, unknown> {
136
+ let preprocessedArgs = preprocessArgs(rawArgs, { flags: {}, aliases: {} });
137
+
138
+ const positionalConfig = command.meta?.positional ? parsePositionalConfig(command.meta.positional) : [];
139
+
140
+ if (positionalConfig.length > 0) {
141
+ let argIndex = 0;
142
+ for (let i = 0; i < positionalConfig.length; i++) {
143
+ const { name, variadic } = positionalConfig[i]!;
144
+ if (argIndex >= positionalArgs.length) break;
145
+
146
+ if (variadic) {
147
+ const remainingPositionals = positionalConfig.slice(i + 1);
148
+ const nonVariadicAfter = remainingPositionals.filter((p) => !p.variadic).length;
149
+ const variadicEnd = positionalArgs.length - nonVariadicAfter;
150
+ preprocessedArgs[name] = positionalArgs.slice(argIndex, variadicEnd);
151
+ argIndex = variadicEnd;
152
+ } else if (i === positionalConfig.length - 1 && positionalArgs.length > argIndex + 1) {
153
+ preprocessedArgs[name] = positionalArgs.slice(argIndex).join(' ');
154
+ argIndex = positionalArgs.length;
155
+ } else {
156
+ preprocessedArgs[name] = positionalArgs[argIndex];
157
+ argIndex++;
158
+ }
159
+ }
160
+ }
161
+
162
+ if (command.argsSchema) {
163
+ preprocessedArgs = coerceArgs(preprocessedArgs, command.argsSchema);
164
+ }
165
+
166
+ return preprocessedArgs;
167
+ }
168
+
169
+ /**
170
+ * Detects unknown options in args that aren't defined in the schema.
171
+ * Returns unknown key info with suggestions, or empty array if schema is loose.
172
+ */
173
+ export function checkUnknownArgs(command: AnyPadroneCommand, preprocessedArgs: Record<string, unknown>): { key: string }[] {
174
+ if (!command.argsSchema) {
175
+ const unknowns: { key: string }[] = [];
176
+ for (const key of Object.keys(preprocessedArgs)) {
177
+ unknowns.push({ key });
178
+ }
179
+ return unknowns;
180
+ }
181
+
182
+ const argsMeta = command.meta?.fields;
183
+ const { flags, aliases } = extractSchemaMetadata(command.argsSchema, argsMeta, command.meta?.autoAlias);
184
+
185
+ return detectUnknownArgs(preprocessedArgs, command.argsSchema, flags, aliases);
186
+ }
187
+
188
+ /**
189
+ * Validates preprocessed arguments against the command's schema.
190
+ * First checks for unknown args (strict by default), then runs schema validation.
191
+ * Returns sync or async result depending on the schema's validate method.
192
+ */
193
+ export function validateCommandArgs(command: AnyPadroneCommand, preprocessedArgs: Record<string, unknown>) {
194
+ const unknownArgs = checkUnknownArgs(command, preprocessedArgs);
195
+ if (unknownArgs.length > 0) {
196
+ const issues: StandardSchemaV1.Issue[] = unknownArgs.map(({ key }) => ({
197
+ path: [key],
198
+ message: `Unknown option: "${key}"`,
199
+ }));
200
+ return { args: undefined, argsResult: { issues } as any };
201
+ }
202
+
203
+ const argsParsed = command.argsSchema ? command.argsSchema['~standard'].validate(preprocessedArgs) : { value: {} };
204
+
205
+ const buildResult = (parsed: StandardSchemaV1.Result<unknown>) => ({
206
+ args: parsed.issues ? undefined : (parsed.value as any),
207
+ argsResult: parsed as any,
208
+ });
209
+
210
+ return thenMaybe(argsParsed, buildResult);
211
+ }
212
+
213
+ /**
214
+ * Returns the list of known option names from a command's schema (for fuzzy suggestion).
215
+ */
216
+ export function getKnownOptionNames(command: AnyPadroneCommand): string[] {
217
+ if (!command.argsSchema) return [];
218
+ try {
219
+ const js = getJsonSchema(command.argsSchema) as Record<string, any>;
220
+ if (js.type === 'object' && js.properties) return Object.keys(js.properties);
221
+ } catch {
222
+ /* ignore */
223
+ }
224
+ return [];
225
+ }
226
+
227
+ /**
228
+ * Formats validation issue messages for display.
229
+ */
230
+ export function formatIssueMessages(issues: readonly StandardSchemaV1.Issue[]): string {
231
+ return issues.map((i) => ` - ${i.path?.join('.') || 'root'}: ${i.message}`).join('\n');
232
+ }
233
+
234
+ /**
235
+ * Core validate function for parse() — preprocesses and validates CLI args.
236
+ * Used by the parse program method (lighter weight than the full exec pipeline).
237
+ * External data sources (stdin, env, config) are not resolved here — use eval() for that.
238
+ */
239
+ export function coreValidateForParse(
240
+ command: AnyPadroneCommand,
241
+ rawArgs: Record<string, unknown>,
242
+ positionalArgs: string[],
243
+ ): InterceptorValidateResult | Promise<InterceptorValidateResult> {
244
+ const preprocessedArgs = buildCommandArgs(command, rawArgs, positionalArgs);
245
+ const validated = validateCommandArgs(command, preprocessedArgs);
246
+ return thenMaybe(validated, (v) => v as InterceptorValidateResult);
247
+ }
package/src/docs/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
- import { commandSymbol } from '../command-utils.ts';
4
- import type { HelpArgumentInfo, HelpInfo, HelpPositionalInfo, HelpSubcommandInfo } from '../formatter.ts';
5
- import { getHelpInfo } from '../help.ts';
6
- import type { AnyPadroneCommand } from '../types.ts';
3
+ import { getCommand } from '../core/commands.ts';
4
+ import type { HelpArgumentInfo, HelpInfo, HelpPositionalInfo, HelpSubcommandInfo } from '../output/formatter.ts';
5
+ import { getHelpInfo } from '../output/help.ts';
6
+ import type { AnyPadroneCommand } from '../types/index.ts';
7
7
 
8
8
  // ============================================================================
9
9
  // Types
@@ -220,6 +220,18 @@ function generateMarkdownPage(info: HelpInfo, depth: number, frontmatterFn?: Doc
220
220
  lines.push('```');
221
221
  lines.push('');
222
222
 
223
+ // Examples
224
+ if (info.examples?.length) {
225
+ lines.push('## Examples');
226
+ lines.push('');
227
+ lines.push('```');
228
+ for (const ex of info.examples) {
229
+ lines.push(`$ ${ex}`);
230
+ }
231
+ lines.push('```');
232
+ lines.push('');
233
+ }
234
+
223
235
  // Subcommands
224
236
  if (info.subcommands?.length) {
225
237
  const visibleSubs = info.subcommands.filter((s) => !s.hidden);
@@ -307,6 +319,12 @@ function generateHtmlPage(info: HelpInfo, depth: number): string {
307
319
  sections.push(' <h2>Usage</h2>');
308
320
  sections.push(` <pre><code>${escapeHtml(usageParts.join(' '))}</code></pre>`);
309
321
 
322
+ // Examples
323
+ if (info.examples?.length) {
324
+ sections.push(' <h2>Examples</h2>');
325
+ sections.push(` <pre><code>${info.examples.map((ex) => `$ ${escapeHtml(ex)}`).join('\n')}</code></pre>`);
326
+ }
327
+
310
328
  // Subcommands
311
329
  if (info.subcommands?.length) {
312
330
  const visibleSubs = info.subcommands.filter((s) => !s.hidden);
@@ -381,7 +399,7 @@ function generateHtmlPage(info: HelpInfo, depth: number): string {
381
399
  }
382
400
 
383
401
  // ============================================================================
384
- // Man Page Generator
402
+ // Man Page Generator (experimental)
385
403
  // ============================================================================
386
404
 
387
405
  function escapeMan(text: string): string {
@@ -418,6 +436,15 @@ function generateManPage(info: HelpInfo, _depth: number, programName: string): s
418
436
  lines.push(escapeMan(info.description));
419
437
  }
420
438
 
439
+ // EXAMPLES
440
+ if (info.examples?.length) {
441
+ lines.push('.SH EXAMPLES');
442
+ for (const ex of info.examples) {
443
+ lines.push('.PP');
444
+ lines.push(`.nf\n$ ${escapeMan(ex)}\n.fi`);
445
+ }
446
+ }
447
+
421
448
  // COMMANDS
422
449
  if (info.subcommands?.length) {
423
450
  const visibleSubs = info.subcommands.filter((s) => !s.hidden);
@@ -517,11 +544,6 @@ function generateMarkdownIndex(rootInfo: HelpInfo, allInfos: HelpInfo[]): string
517
544
  // Main Entry Point
518
545
  // ============================================================================
519
546
 
520
- function resolveCommand(programOrCommand: object): AnyPadroneCommand {
521
- if (commandSymbol in programOrCommand) return (programOrCommand as any)[commandSymbol] as AnyPadroneCommand;
522
- return programOrCommand as AnyPadroneCommand;
523
- }
524
-
525
547
  /**
526
548
  * Generate documentation for a Padrone CLI program or command tree.
527
549
  * Accepts either a PadroneProgram (from createPadrone()) or a raw AnyPadroneCommand.
@@ -529,7 +551,7 @@ function resolveCommand(programOrCommand: object): AnyPadroneCommand {
529
551
  export function generateDocs(program: object, options: DocsOptions = {}): DocsResult {
530
552
  const { format = 'markdown', output, includeHidden = false, frontmatter, overwrite = true, dryRun = false } = options;
531
553
 
532
- const cmd = resolveCommand(program);
554
+ const cmd = getCommand(program);
533
555
  const allInfos = collectAllHelpInfo(cmd, includeHidden);
534
556
  const rootInfo = allInfos[0]!;
535
557
  const programName = cmd.name || 'program';
@@ -605,3 +627,94 @@ export function generateDocs(program: object, options: DocsOptions = {}): DocsRe
605
627
 
606
628
  return result;
607
629
  }
630
+
631
+ // ============================================================================
632
+ // Man Page Installation
633
+ // ============================================================================
634
+
635
+ export type SetupManPagesResult = {
636
+ /** Directory where man pages were written. */
637
+ dir: string;
638
+ /** Man page files that were written. */
639
+ written: string[];
640
+ /** Whether existing pages were overwritten (true) or newly created (false). */
641
+ updated: boolean;
642
+ };
643
+
644
+ /**
645
+ * Returns the local man page directory for the given section.
646
+ * Uses `~/.local/share/man/man<section>` (XDG convention).
647
+ */
648
+ async function getManPageDir(section = 1): Promise<string> {
649
+ const { homedir } = await import('node:os');
650
+ return join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'man', `man${section}`);
651
+ }
652
+
653
+ /**
654
+ * Converts a command name to a man page filename.
655
+ * "myapp" → "myapp.1", "myapp deploy" → "myapp-deploy.1"
656
+ */
657
+ function manPageFilename(commandName: string, section = 1): string {
658
+ return `${commandName.replace(/\s+/g, '-')}.${section}`;
659
+ }
660
+
661
+ /**
662
+ * Installs man pages for a Padrone CLI program into the local man directory.
663
+ * Generates man pages for all commands and writes them to `~/.local/share/man/man1/`.
664
+ *
665
+ * After installation, `man <program>` and `man <program>-<subcommand>` should work
666
+ * (assuming `~/.local/share/man` is in `MANPATH` or `manpath` picks it up).
667
+ */
668
+ export async function setupManPages(program: object): Promise<SetupManPagesResult> {
669
+ const cmd = getCommand(program);
670
+ const allInfos = collectAllHelpInfo(cmd, false);
671
+ const programName = cmd.name || 'program';
672
+ const manDir = await getManPageDir(1);
673
+
674
+ mkdirSync(manDir, { recursive: true });
675
+
676
+ const written: string[] = [];
677
+ let updated = false;
678
+
679
+ for (let i = 0; i < allInfos.length; i++) {
680
+ const info = allInfos[i]!;
681
+ const depth = i === 0 ? 0 : info.name.split(/\s+/).length;
682
+ const commandName = info.name === '<root>' || !info.name ? programName : info.name;
683
+ const filename = manPageFilename(commandName);
684
+ const fullPath = join(manDir, filename);
685
+
686
+ if (existsSync(fullPath)) updated = true;
687
+
688
+ const content = generateManPage(info, depth, programName);
689
+ writeFileSync(fullPath, content, 'utf-8');
690
+ written.push(filename);
691
+ }
692
+
693
+ return { dir: manDir, written, updated };
694
+ }
695
+
696
+ /**
697
+ * Removes installed man pages for a Padrone CLI program.
698
+ */
699
+ export async function removeManPages(program: object): Promise<{ dir: string; removed: string[] }> {
700
+ const { unlinkSync } = await import('node:fs');
701
+ const cmd = getCommand(program);
702
+ const allInfos = collectAllHelpInfo(cmd, false);
703
+ const programName = cmd.name || 'program';
704
+ const manDir = await getManPageDir(1);
705
+ const removed: string[] = [];
706
+
707
+ for (let i = 0; i < allInfos.length; i++) {
708
+ const info = allInfos[i]!;
709
+ const commandName = info.name === '<root>' || !info.name ? programName : info.name;
710
+ const filename = manPageFilename(commandName);
711
+ const fullPath = join(manDir, filename);
712
+
713
+ if (existsSync(fullPath)) {
714
+ unlinkSync(fullPath);
715
+ removed.push(filename);
716
+ }
717
+ }
718
+
719
+ return { dir: manDir, removed };
720
+ }