padrone 1.5.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 (138) hide show
  1. package/CHANGELOG.md +36 -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-CL63UOzt.mjs} +26 -3
  16. package/dist/errors-CL63UOzt.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-B5Kk83of.mjs} +151 -37
  20. package/dist/help-B5Kk83of.mjs.map +1 -0
  21. package/dist/{types-Ch8Mk6Qb.d.mts → index-BaU3X6dY.d.mts} +621 -750
  22. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  23. package/dist/index.d.mts +735 -37
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3409 -1563
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{mcp-mLWIdUIu.mjs → mcp-BM-d0nZi.mjs} +13 -15
  28. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  29. package/dist/{serve-B0u43DK7.mjs → serve-Bk0JUlCj.mjs} +12 -14
  30. package/dist/serve-Bk0JUlCj.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 +268 -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 +95 -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 +43 -0
  72. package/src/extension/ink.ts +93 -0
  73. package/src/extension/interactive.ts +106 -0
  74. package/src/extension/logger.ts +214 -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 +111 -30
  98. package/src/{formatter.ts → output/formatter.ts} +131 -31
  99. package/src/{help.ts → output/help.ts} +22 -8
  100. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  101. package/src/schema/zod.ts +50 -0
  102. package/src/test.ts +2 -276
  103. package/src/types/args-meta.ts +151 -0
  104. package/src/types/builder.ts +697 -0
  105. package/src/types/command.ts +157 -0
  106. package/src/types/index.ts +59 -0
  107. package/src/types/interceptor.ts +296 -0
  108. package/src/types/preferences.ts +83 -0
  109. package/src/types/result.ts +71 -0
  110. package/src/types/schema.ts +19 -0
  111. package/src/util/dotenv.ts +244 -0
  112. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  113. package/src/{stream.ts → util/stream.ts} +27 -1
  114. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  115. package/src/{type-utils.ts → util/type-utils.ts} +71 -33
  116. package/src/util/utils.ts +51 -0
  117. package/src/zod.ts +1 -50
  118. package/dist/args-D5PNDyNu.mjs.map +0 -1
  119. package/dist/chunk-CjcI7cDX.mjs +0 -15
  120. package/dist/command-utils-B1D-HqCd.mjs +0 -1117
  121. package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
  122. package/dist/completion.d.mts +0 -64
  123. package/dist/completion.d.mts.map +0 -1
  124. package/dist/completion.mjs.map +0 -1
  125. package/dist/errors-BiVrBgi6.mjs.map +0 -1
  126. package/dist/formatter-DtHzbP22.d.mts.map +0 -1
  127. package/dist/help-bbmu9-qd.mjs.map +0 -1
  128. package/dist/mcp-mLWIdUIu.mjs.map +0 -1
  129. package/dist/serve-B0u43DK7.mjs.map +0 -1
  130. package/dist/stream-BcC146Ud.mjs.map +0 -1
  131. package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
  132. package/dist/update-check-CFX1FV3v.mjs.map +0 -1
  133. package/src/command-utils.ts +0 -882
  134. package/src/create.ts +0 -1829
  135. package/src/runtime.ts +0 -497
  136. package/src/types.ts +0 -1291
  137. package/src/utils.ts +0 -140
  138. /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
@@ -0,0 +1,49 @@
1
+ import type { ShellType } from '#src/util/shell-utils.ts';
2
+ import { resolveAllCommands } from '../core/commands.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 CompletionArgs = { shell?: string; setup?: boolean };
12
+
13
+ type CompletionCommand = PadroneCommand<'completion', '', PadroneSchema<CompletionArgs>, string, [], [], true>;
14
+
15
+ export type WithCompletion<T> = WithCommand<T, 'completion', CompletionCommand>;
16
+
17
+ // ── Extension ────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Extension that adds the `completion` command for shell completion script generation.
21
+ *
22
+ * Usage:
23
+ * ```ts
24
+ * createPadrone('my-cli').extend(padroneCompletion())
25
+ * ```
26
+ */
27
+ export function padroneCompletion(): <T extends CommandTypesBase>(builder: T) => WithCompletion<T> {
28
+ return ((builder: AnyPadroneBuilder) =>
29
+ builder.command('completion', (c) =>
30
+ c
31
+ .configure({ description: 'Generate shell completion scripts', hidden: true })
32
+ .arguments(passthroughSchema({ shell: 'string', setup: 'boolean' }), { positional: ['shell'] })
33
+ .async()
34
+ .action(async (args, ctx) => {
35
+ const rootCommand = getRootCommand(ctx.command);
36
+ resolveAllCommands(rootCommand);
37
+ const { detectShell, generateCompletionOutput, setupCompletions } = await import('../feature/completion.ts');
38
+ const shell = args.shell as ShellType;
39
+ const setup = args.setup;
40
+ if (setup) {
41
+ const resolvedShell = shell ?? (await detectShell());
42
+ if (!resolvedShell) throw new Error('Could not detect shell. Specify one: completion bash --setup');
43
+ const setupResult = await setupCompletions(rootCommand.name, resolvedShell);
44
+ return `${setupResult.updated ? 'Updated' : 'Added'} ${rootCommand.name} completions in ${setupResult.file}`;
45
+ }
46
+ return generateCompletionOutput(rootCommand, shell);
47
+ }),
48
+ )) as any;
49
+ }
@@ -0,0 +1,262 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { applyValues } from '../core/args.ts';
3
+ import { ConfigError } from '../core/errors.ts';
4
+ import { defineInterceptor } from '../core/interceptors.ts';
5
+ import { thenMaybe } from '../core/results.ts';
6
+ import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';
7
+ import { getRootCommand } from '../util/utils.ts';
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────────
10
+
11
+ export type PadroneConfigOptions = {
12
+ /** Config file names to auto-detect (e.g. `['config.json', '.myapprc']`). First found is used. */
13
+ files?: string | string[];
14
+ /** Schema to validate and transform config file data into the args shape. */
15
+ schema?: StandardSchemaV1;
16
+ /** Disable this extension. */
17
+ disabled?: boolean;
18
+ /** Whether to add `--config` / `-c` flag support. Defaults to `true`. */
19
+ flag?: boolean;
20
+ /** Whether subcommands inherit this interceptor. Defaults to `true`. */
21
+ inherit?: boolean;
22
+ /**
23
+ * Search for config files in the user's platform-specific config directory.
24
+ * - `true` — use the program name as the subdirectory (e.g. program `'myapp'` → `~/.config/myapp/`).
25
+ * - `string` — use a custom app name as the subdirectory.
26
+ * - `false` — disable (default).
27
+ *
28
+ * Directories searched (after cwd):
29
+ * - **Linux**: `$XDG_CONFIG_HOME/<app>` or `~/.config/<app>`
30
+ * - **macOS**: `~/Library/Application Support/<app>` (or `$XDG_CONFIG_HOME/<app>` when set)
31
+ * - **Windows**: `%APPDATA%\<app>`
32
+ *
33
+ * Config files found in cwd always take precedence over XDG paths.
34
+ */
35
+ xdg?: string | boolean;
36
+ /**
37
+ * Custom config loader. When provided, replaces the built-in file system loader.
38
+ * Useful for testing or non-CLI environments.
39
+ */
40
+ loadConfig?: (
41
+ files: string | string[],
42
+ xdgAppName?: string,
43
+ ) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
44
+ };
45
+
46
+ // ── File system config loader ───────────────────────────────────────────
47
+
48
+ // Lazily resolved Node.js modules — cached after first import to keep loadConfig sync after initialization.
49
+ let _fs: typeof import('node:fs') | undefined;
50
+ let _path: typeof import('node:path') | undefined;
51
+
52
+ async function initNodeModules(): Promise<void> {
53
+ if (_fs && _path) return;
54
+ _fs = await import('node:fs');
55
+ _path = await import('node:path');
56
+ }
57
+
58
+ // Eagerly start caching node modules so loadConfig is sync by the time it's called.
59
+ try {
60
+ if (typeof process !== 'undefined') initNodeModules();
61
+ } catch {
62
+ // Non-CLI environments (browser, edge) — ignore
63
+ }
64
+
65
+ function getUserConfigDir(path: typeof import('node:path'), appName: string): string | undefined {
66
+ const platform = process.platform;
67
+
68
+ // Respect XDG_CONFIG_HOME on all platforms when explicitly set
69
+ const xdgHome = process.env.XDG_CONFIG_HOME;
70
+ if (xdgHome) return path.join(xdgHome, appName);
71
+
72
+ const home = process.env.HOME || process.env.USERPROFILE;
73
+ if (!home) return undefined;
74
+
75
+ if (platform === 'win32') {
76
+ const appData = process.env.APPDATA;
77
+ return appData ? path.join(appData, appName) : path.join(home, 'AppData', 'Roaming', appName);
78
+ }
79
+ if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', appName);
80
+
81
+ // Linux and other Unix — default XDG path
82
+ return path.join(home, '.config', appName);
83
+ }
84
+
85
+ function resolveConfigPath(fs: any, path: any, cwd: string, files: string | string[], xdgAppName?: string): string | undefined {
86
+ if (typeof files === 'string') {
87
+ const abs = path.isAbsolute(files) ? files : path.resolve(cwd, files);
88
+ if (!fs.existsSync(abs)) {
89
+ console.error(`Config file not found: ${abs}`);
90
+ return undefined;
91
+ }
92
+ return abs;
93
+ }
94
+
95
+ // Search in cwd first
96
+ for (const candidate of files) {
97
+ const abs = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
98
+ if (fs.existsSync(abs)) return abs;
99
+ }
100
+
101
+ // Then search in the user config directory (XDG / platform-specific)
102
+ if (xdgAppName) {
103
+ const configDir = getUserConfigDir(path, xdgAppName);
104
+ if (configDir) {
105
+ for (const candidate of files) {
106
+ const abs = path.join(configDir, candidate);
107
+ if (fs.existsSync(abs)) return abs;
108
+ }
109
+ }
110
+ }
111
+
112
+ return undefined;
113
+ }
114
+
115
+ function loadConfigSync(
116
+ fs: typeof import('node:fs'),
117
+ path: typeof import('node:path'),
118
+ files: string | string[],
119
+ xdgAppName?: string,
120
+ ): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> {
121
+ const cwd = process.cwd();
122
+ const absolutePath = resolveConfigPath(fs, path, cwd, files, xdgAppName);
123
+ if (!absolutePath) return undefined;
124
+
125
+ const getContent = () => fs.readFileSync(absolutePath, 'utf-8');
126
+ const ext = path.extname(absolutePath).toLowerCase();
127
+
128
+ if (ext === '.yaml' || ext === '.yml') return Bun.YAML.parse(getContent()) as any;
129
+ if (ext === '.toml') return Bun.TOML.parse(getContent()) as any;
130
+ if (ext === '.jsonc') return Bun.JSONC.parse(getContent()) as any;
131
+ if (ext === '.json') {
132
+ if (Bun.JSONC) return Bun.JSONC.parse(getContent()) as any;
133
+ try {
134
+ return JSON.parse(getContent());
135
+ } catch {
136
+ return Bun.JSONC.parse(getContent()) as any;
137
+ }
138
+ }
139
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '.ts' || ext === '.cts' || ext === '.mts') {
140
+ return import(absolutePath).then((mod) => mod.default ?? mod);
141
+ }
142
+
143
+ // Unknown extension — try JSON
144
+ try {
145
+ return JSON.parse(getContent());
146
+ } catch {
147
+ console.error(`Unable to parse config file: ${absolutePath}`);
148
+ return undefined;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Built-in config file loader. Directly accesses the file system.
154
+ * Returns `undefined` in non-CLI environments where `node:fs` is unavailable.
155
+ */
156
+ function loadConfig(
157
+ files: string | string[],
158
+ xdgAppName?: string,
159
+ ): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> {
160
+ if (typeof process === 'undefined') return undefined;
161
+
162
+ try {
163
+ if (_fs && _path) return loadConfigSync(_fs, _path, files, xdgAppName);
164
+ return initNodeModules().then(() => loadConfigSync(_fs!, _path!, files, xdgAppName));
165
+ } catch {
166
+ return undefined;
167
+ }
168
+ }
169
+
170
+ // ── Extension ────────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * Extension that handles config file loading, validation, and merging into command arguments.
174
+ *
175
+ * Features:
176
+ * - `--config` / `-c` flag for explicit config file path (can be disabled via `flag: false`)
177
+ * - Auto-detection of config files from a list of candidate names
178
+ * - Optional schema validation and transformation of config data
179
+ * - Directly accesses the file system (gracefully no-ops in non-CLI environments)
180
+ *
181
+ * Config values have the lowest precedence (CLI > stdin > env > config).
182
+ *
183
+ * Not included in the default built-in extensions — must be explicitly added:
184
+ * ```ts
185
+ * createPadrone('my-cli')
186
+ * .extend(padroneConfig({
187
+ * files: ['config.json', '.myapprc'],
188
+ * schema: z.object({ port: z.number(), host: z.string() }),
189
+ * }))
190
+ * ```
191
+ */
192
+ export function padroneConfig(options?: PadroneConfigOptions): <T extends CommandTypesBase>(builder: T) => T {
193
+ if (options?.disabled) {
194
+ const disabled = defineInterceptor({ id: 'padrone:config', name: 'padrone:config', order: -999, disabled: true }, () => ({}));
195
+ return ((builder: AnyPadroneBuilder) => builder.intercept(disabled)) as any;
196
+ }
197
+
198
+ const configFiles = options?.files ? (Array.isArray(options.files) ? options.files : [options.files]) : undefined;
199
+ const configSchema = options?.schema;
200
+ const flagEnabled = options?.flag !== false;
201
+ const inherit = options?.inherit;
202
+ const xdgOption = options?.xdg;
203
+ const configLoader = options?.loadConfig ?? loadConfig;
204
+
205
+ const interceptor = defineInterceptor(
206
+ { id: 'padrone:config', name: 'padrone:config', order: -999, ...(inherit === false && { inherit: false }) },
207
+ () => ({
208
+ validate(ctx: InterceptorValidateContext, next) {
209
+ // Extract --config / -c from rawArgs
210
+ let explicitConfigPath: string | undefined;
211
+ if (flagEnabled) {
212
+ explicitConfigPath = (ctx.rawArgs.config ?? ctx.rawArgs.c) as string | undefined;
213
+ if (typeof explicitConfigPath === 'string') {
214
+ delete ctx.rawArgs.config;
215
+ delete ctx.rawArgs.c;
216
+ }
217
+ }
218
+
219
+ // Skip entirely when there's nothing to load
220
+ if (!explicitConfigPath && !configFiles) return next();
221
+
222
+ // Resolve XDG app name: true → derive from root command name, string → use as-is
223
+ let xdgAppName: string | undefined;
224
+ if (typeof xdgOption === 'string') xdgAppName = xdgOption;
225
+ else if (xdgOption === true) xdgAppName = getRootCommand(ctx.command).name;
226
+
227
+ // Load config data: explicit --config flag takes priority, then auto-detect
228
+ const configDataOrPromise = configLoader(explicitConfigPath ?? configFiles ?? [], xdgAppName);
229
+
230
+ const applyConfig = (configData: Record<string, unknown> | undefined) => {
231
+ if (!configData) return next();
232
+
233
+ // Validate against schema if provided
234
+ if (configSchema) {
235
+ const validated = configSchema['~standard'].validate(configData);
236
+ return thenMaybe(validated, (result) => {
237
+ if (result.issues) {
238
+ const issueMessages = result.issues
239
+ .map((i: StandardSchemaV1.Issue) => ` - ${i.path?.join('.') || 'root'}: ${i.message}`)
240
+ .join('\n');
241
+ throw new ConfigError(`Invalid config file:\n${issueMessages}`, {
242
+ command: ctx.command.path || ctx.command.name,
243
+ });
244
+ }
245
+ const validatedData = result.value as Record<string, unknown>;
246
+ const mergedRawArgs = applyValues(ctx.rawArgs, validatedData);
247
+ return next({ rawArgs: mergedRawArgs });
248
+ });
249
+ }
250
+
251
+ // No schema — pass through as-is
252
+ const mergedRawArgs = applyValues(ctx.rawArgs, configData);
253
+ return next({ rawArgs: mergedRawArgs });
254
+ };
255
+
256
+ return thenMaybe(configDataOrPromise, applyConfig);
257
+ },
258
+ }),
259
+ );
260
+
261
+ return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
262
+ }
@@ -0,0 +1,101 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { applyValues } from '../core/args.ts';
3
+ import { defineInterceptor } from '../core/interceptors.ts';
4
+ import { thenMaybe } from '../core/results.ts';
5
+ import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';
6
+ import type { LoadEnvFilesOptions } from '../util/dotenv.ts';
7
+ import { loadEnvFiles } from '../util/dotenv.ts';
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────────
10
+
11
+ export type PadroneEnvOptions = {
12
+ /** Env modes to load (e.g. `['production']`). Loads `.env.{mode}` files. */
13
+ modes?: string[];
14
+ /** Whether to load `.env.local` and `.env.{mode}.local` files. @default true */
15
+ local?: boolean;
16
+ /** Directory to search for `.env` files. @default process.cwd() */
17
+ dir?: string;
18
+ /** When `true`, file values override `process.env` values. @default false */
19
+ override?: boolean;
20
+ /** When `false`, the base `.env` (and `.env.local`) files are not loaded. @default true */
21
+ base?: boolean;
22
+ };
23
+
24
+ // ── Helpers ──────────────────────────────────────────────────────────────
25
+
26
+ function isSchema(value: unknown): value is StandardSchemaV1 {
27
+ return value != null && typeof value === 'object' && '~standard' in value;
28
+ }
29
+
30
+ // ── Extension ────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Extension that reads environment variables, validates them against a schema,
34
+ * and merges the transformed values into command arguments.
35
+ *
36
+ * Supports loading `.env` files with mode-based overrides and variable expansion.
37
+ *
38
+ * ```ts
39
+ * // Schema only (reads process.env)
40
+ * .extend(padroneEnv(
41
+ * z.object({ PORT: z.string() }).transform(e => ({ port: Number(e.PORT) }))
42
+ * ))
43
+ *
44
+ * // Schema + .env file loading
45
+ * .extend(padroneEnv(
46
+ * z.object({ PORT: z.string() }).transform(e => ({ port: Number(e.PORT) })),
47
+ * { modes: ['production'] }
48
+ * ))
49
+ *
50
+ * // .env file loading only (no schema validation)
51
+ * .extend(padroneEnv({ modes: ['production'] }))
52
+ * ```
53
+ *
54
+ * Env values have lower precedence than CLI args and stdin, but higher than config files.
55
+ */
56
+ export function padroneEnv(schema: StandardSchemaV1): <T extends CommandTypesBase>(builder: T) => T;
57
+ export function padroneEnv(schema: StandardSchemaV1, options: PadroneEnvOptions): <T extends CommandTypesBase>(builder: T) => T;
58
+ export function padroneEnv(options: PadroneEnvOptions): <T extends CommandTypesBase>(builder: T) => T;
59
+ export function padroneEnv(
60
+ schemaOrOptions: StandardSchemaV1 | PadroneEnvOptions,
61
+ maybeOptions?: PadroneEnvOptions,
62
+ ): <T extends CommandTypesBase>(builder: T) => T {
63
+ const schema = isSchema(schemaOrOptions) ? schemaOrOptions : undefined;
64
+ const options = isSchema(schemaOrOptions) ? maybeOptions : schemaOrOptions;
65
+ const hasFiles = options?.modes !== undefined;
66
+ const fileOptions: LoadEnvFilesOptions | undefined = hasFiles ? options : undefined;
67
+ const override = options?.override ?? false;
68
+
69
+ const interceptor = defineInterceptor({ id: 'padrone:env', name: 'padrone:env', order: -1000 }, () => ({
70
+ validate(ctx: InterceptorValidateContext, next) {
71
+ const processEnv = ctx.runtime.env();
72
+
73
+ const applyEnv = (envFromFiles: Record<string, string>) => {
74
+ const rawEnv = override ? { ...processEnv, ...envFromFiles } : { ...envFromFiles, ...processEnv };
75
+
76
+ if (schema) {
77
+ const envValidated = schema['~standard'].validate(rawEnv);
78
+ return thenMaybe(envValidated, (result) => {
79
+ if (result.issues || !result.value) return next();
80
+ return next({ rawArgs: applyValues(ctx.rawArgs, result.value as Record<string, unknown>) });
81
+ });
82
+ }
83
+
84
+ // No schema — merge file env values directly into rawArgs
85
+ if (Object.keys(envFromFiles).length > 0) {
86
+ return next({ rawArgs: applyValues(ctx.rawArgs, envFromFiles) });
87
+ }
88
+ return next();
89
+ };
90
+
91
+ if (hasFiles) {
92
+ const loaded = loadEnvFiles(fileOptions!, processEnv);
93
+ return thenMaybe(loaded, applyEnv);
94
+ }
95
+
96
+ return applyEnv({});
97
+ },
98
+ }));
99
+
100
+ return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
101
+ }
@@ -0,0 +1,192 @@
1
+ import { resolveAllCommands, resolveCommand } from '../core/commands.ts';
2
+ import { RoutingError, ValidationError } from '../core/errors.ts';
3
+ import { defineInterceptor } from '../core/interceptors.ts';
4
+ import { thenMaybe } from '../core/results.ts';
5
+ import { formatIssueMessages } from '../core/validate.ts';
6
+ import type { HelpDetail, HelpFormat } from '../output/formatter.ts';
7
+ import { generateHelp } from '../output/help.ts';
8
+ import type { AnyPadroneBuilder, AnyPadroneCommand, CommandTypesBase, PadroneCommand } from '../types/index.ts';
9
+ import type { PadroneSchema } from '../types/schema.ts';
10
+ import type { WithCommand } from '../util/type-utils.ts';
11
+ import { getRootCommand } from '../util/utils.ts';
12
+ import { findCommandInTree, passthroughSchema } from './utils.ts';
13
+
14
+ // ── Types ────────────────────────────────────────────────────────────────
15
+
16
+ type HelpArgs = { command?: string[]; detail?: HelpDetail; format?: HelpFormat; all?: boolean };
17
+
18
+ export type HelpCommand = PadroneCommand<'help', '', PadroneSchema<HelpArgs>, string, [], ['h', ''], false>;
19
+
20
+ export type WithHelp<T> = WithCommand<T, 'help', HelpCommand>;
21
+
22
+ // ── Interceptor ─────────────────────────────────────────────────────────
23
+
24
+ const helpInterceptor = defineInterceptor({ id: 'padrone:help', name: 'padrone:help', order: -1000 }, () => {
25
+ let helpText: string | undefined;
26
+ let showDefaultHelp = false;
27
+
28
+ return {
29
+ parse(ctx, next) {
30
+ return thenMaybe(next(), (res) => {
31
+ const hasHelpFlag = res.rawArgs.help || res.rawArgs.h;
32
+ const reverseHelp = !hasHelpFlag && res.positionalArgs?.length > 0 && res.positionalArgs[res.positionalArgs.length - 1] === 'help';
33
+
34
+ if (hasHelpFlag || reverseHelp) {
35
+ delete res.rawArgs.help;
36
+ delete res.rawArgs.h;
37
+
38
+ const detail = res.rawArgs.detail as HelpDetail | undefined;
39
+ const format = res.rawArgs.format as HelpFormat | undefined;
40
+ const all = res.rawArgs.all as boolean | undefined;
41
+ delete res.rawArgs.detail;
42
+ delete res.rawArgs.format;
43
+ delete res.rawArgs.all;
44
+ delete res.rawArgs.d;
45
+ delete res.rawArgs.f;
46
+
47
+ const rootCommand = getRootCommand(res.command);
48
+ resolveAllCommands(rootCommand);
49
+
50
+ helpText = generateHelp(rootCommand, res.command, {
51
+ detail,
52
+ format: format ?? ctx.runtime.format,
53
+ theme: ctx.runtime.theme,
54
+ all,
55
+ terminal: ctx.runtime.terminal,
56
+ env: ctx.runtime.env(),
57
+ });
58
+ return res;
59
+ }
60
+
61
+ // Track whether the parsed command has no action (for default help in execute phase)
62
+ if (helpText === undefined) {
63
+ const { command } = res;
64
+ const hasSubcommands = command.commands && command.commands.length > 0;
65
+ const hasSchema = command.argsSchema != null;
66
+ const hasUnmatchedTerms = res.positionalArgs?.length > 0 && !command.meta?.positional?.length;
67
+ if (!command.action && (hasSubcommands || !hasSchema) && !hasUnmatchedTerms) {
68
+ showDefaultHelp = true;
69
+ }
70
+ }
71
+
72
+ return res;
73
+ });
74
+ },
75
+ validate(_ctx, next) {
76
+ if (helpText !== undefined) return { args: undefined as any, argsResult: { value: undefined } as any };
77
+ return next();
78
+ },
79
+ execute(ctx, next) {
80
+ if (helpText !== undefined) return { result: helpText };
81
+ if (showDefaultHelp) {
82
+ const rootCommand = getRootCommand(ctx.command);
83
+ resolveAllCommands(rootCommand);
84
+ return {
85
+ result: generateHelp(rootCommand, ctx.command, {
86
+ format: ctx.runtime.format,
87
+ theme: ctx.runtime.theme,
88
+ terminal: ctx.runtime.terminal,
89
+ env: ctx.runtime.env(),
90
+ }),
91
+ };
92
+ }
93
+ return next();
94
+ },
95
+ error(ctx, next) {
96
+ return thenMaybe(next(), (er) => {
97
+ if (ctx.caller !== 'cli' || !er.error) return er;
98
+
99
+ const rootCommand = getRootCommand(ctx.command);
100
+
101
+ if (er.error instanceof RoutingError) {
102
+ const targetPath = er.error.command;
103
+ const targetCommand = targetPath ? findCommandInTree(targetPath, rootCommand) : undefined;
104
+ const sourceCmd = targetCommand ?? rootCommand;
105
+
106
+ ctx.runtime.error(er.error.message);
107
+
108
+ if (er.error.suggestions.length > 0) {
109
+ const visibleCommands = (sourceCmd.commands ?? []).filter((c: AnyPadroneCommand) => !c.hidden && c.name);
110
+ if (visibleCommands.length > 0) {
111
+ for (const cmd of visibleCommands) resolveCommand(cmd);
112
+ const cmdList = visibleCommands.map((c: AnyPadroneCommand) => c.name).join(', ');
113
+ ctx.runtime.output(`\nAvailable commands: ${cmdList}`);
114
+ }
115
+ } else {
116
+ resolveAllCommands(rootCommand);
117
+ const helpText = generateHelp(rootCommand, sourceCmd, {
118
+ format: ctx.runtime.format,
119
+ theme: ctx.runtime.theme,
120
+ terminal: ctx.runtime.terminal,
121
+ env: ctx.runtime.env(),
122
+ });
123
+ ctx.runtime.error(helpText);
124
+ }
125
+
126
+ return er;
127
+ }
128
+
129
+ if (er.error instanceof ValidationError) {
130
+ const targetPath = er.error.command;
131
+ const targetCommand = targetPath ? findCommandInTree(targetPath, rootCommand) : undefined;
132
+ const issueMessages = formatIssueMessages(er.error.issues);
133
+
134
+ resolveAllCommands(rootCommand);
135
+ const helpText = generateHelp(rootCommand, targetCommand ?? rootCommand, {
136
+ format: ctx.runtime.format,
137
+ theme: ctx.runtime.theme,
138
+ terminal: ctx.runtime.terminal,
139
+ env: ctx.runtime.env(),
140
+ });
141
+ ctx.runtime.error(`Validation error:\n${issueMessages}`);
142
+ ctx.runtime.error(helpText);
143
+
144
+ return er;
145
+ }
146
+
147
+ return er;
148
+ });
149
+ },
150
+ };
151
+ });
152
+
153
+ // ── Extension ────────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Extension that adds help support:
157
+ * - `help` command with aliases `h` and `` (empty = executes on root when no subcommand matches)
158
+ * - `--help` / `-h` flags
159
+ * - `<cmd> help` reverse syntax
160
+ * - Default help display when a command has no action
161
+ *
162
+ * Usage:
163
+ * ```ts
164
+ * createPadrone('my-cli').extend(padroneHelp())
165
+ * ```
166
+ */
167
+ export function padroneHelp(): <T extends CommandTypesBase>(builder: T) => WithHelp<T> {
168
+ return ((builder: AnyPadroneBuilder) =>
169
+ builder
170
+ .command(['help', 'h'], (c) =>
171
+ c
172
+ .configure({ description: 'Display help for a command', hidden: true })
173
+ .arguments(passthroughSchema({ command: 'string[]', detail: 'string', format: 'string', all: 'boolean' }), {
174
+ positional: ['...command'],
175
+ })
176
+ .action((args, ctx) => {
177
+ const rootCommand = getRootCommand(ctx.command);
178
+ resolveAllCommands(rootCommand);
179
+ const commandName = args.command?.join(' ');
180
+ const targetCommand = commandName ? findCommandInTree(commandName, rootCommand) : rootCommand;
181
+ return generateHelp(rootCommand, targetCommand ?? rootCommand, {
182
+ detail: args.detail as HelpDetail,
183
+ format: (args.format as HelpFormat) ?? ctx.runtime.format,
184
+ theme: ctx.runtime.theme,
185
+ all: args.all,
186
+ terminal: ctx.runtime.terminal,
187
+ env: ctx.runtime.env(),
188
+ });
189
+ }),
190
+ )
191
+ .intercept(helpInterceptor)) as any;
192
+ }
@@ -0,0 +1,43 @@
1
+ export { padroneAutoOutput } from './auto-output.ts';
2
+ export { padroneColor } from './color.ts';
3
+ export type { WithCompletion } from './completion.ts';
4
+ export { padroneCompletion } from './completion.ts';
5
+ export type { PadroneConfigOptions } from './config.ts';
6
+ export { padroneConfig } from './config.ts';
7
+ export type { PadroneEnvOptions } from './env.ts';
8
+ export { padroneEnv } from './env.ts';
9
+ export type { HelpCommand, WithHelp } from './help.ts';
10
+ export { padroneHelp } from './help.ts';
11
+ export type { InkOptions } from './ink.ts';
12
+ export { isReactElement, padroneInk } from './ink.ts';
13
+ export { padroneInteractive } from './interactive.ts';
14
+ export type { PadroneLogger, PadroneLoggerConfig, PadroneLogLevel, WithLogger } from './logger.ts';
15
+ export { padroneLogger } from './logger.ts';
16
+ export type { WithMan } from './man.ts';
17
+ export { padroneMan } from './man.ts';
18
+ export type { WithMcp } from './mcp.ts';
19
+ export { padroneMcp } from './mcp.ts';
20
+ export type {
21
+ PadroneProgressConfig,
22
+ PadroneProgressDefaults,
23
+ PadroneProgressMessage,
24
+ PadroneProgressMessages,
25
+ WithProgress,
26
+ } from './progress.ts';
27
+ export { padroneProgress } from './progress.ts';
28
+ export type { PadroneProgressRenderer } from './progress-renderer.ts';
29
+ export { createTerminalProgress } from './progress-renderer.ts';
30
+ export type { WithRepl } from './repl.ts';
31
+ export { padroneRepl } from './repl.ts';
32
+ export type { WithServe } from './serve.ts';
33
+ export { padroneServe } from './serve.ts';
34
+ export { padroneSignalHandling } from './signal.ts';
35
+ export { padroneStdin } from './stdin.ts';
36
+ export { padroneSuggestions } from './suggestions.ts';
37
+ export type { PadroneTimingOptions } from './timing.ts';
38
+ export { padroneTiming } from './timing.ts';
39
+ export type { OtelSpan, OtelTracer, OtelTracerProvider, PadroneTracer, PadroneTracingConfig, WithTracing } from './tracing.ts';
40
+ export { padroneTracing } from './tracing.ts';
41
+ export { padroneUpdateCheck } from './update-check.ts';
42
+ export type { VersionCommand, WithVersion } from './version.ts';
43
+ export { padroneVersion } from './version.ts';