padrone 1.0.0 → 1.2.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 (80) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +1 -1
  3. package/README.md +92 -49
  4. package/dist/args-CKNh7Dm9.mjs +175 -0
  5. package/dist/args-CKNh7Dm9.mjs.map +1 -0
  6. package/dist/chunk-y_GBKt04.mjs +5 -0
  7. package/dist/codegen/index.d.mts +305 -0
  8. package/dist/codegen/index.d.mts.map +1 -0
  9. package/dist/codegen/index.mjs +1348 -0
  10. package/dist/codegen/index.mjs.map +1 -0
  11. package/dist/completion.d.mts +64 -0
  12. package/dist/completion.d.mts.map +1 -0
  13. package/dist/completion.mjs +417 -0
  14. package/dist/completion.mjs.map +1 -0
  15. package/dist/docs/index.d.mts +34 -0
  16. package/dist/docs/index.d.mts.map +1 -0
  17. package/dist/docs/index.mjs +404 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-Dvx7jFXr.d.mts +82 -0
  20. package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
  21. package/dist/help-mUIX0T0V.mjs +1195 -0
  22. package/dist/help-mUIX0T0V.mjs.map +1 -0
  23. package/dist/index.d.mts +122 -438
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1240 -1161
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/test.d.mts +112 -0
  28. package/dist/test.d.mts.map +1 -0
  29. package/dist/test.mjs +138 -0
  30. package/dist/test.mjs.map +1 -0
  31. package/dist/types-qrtt0135.d.mts +1037 -0
  32. package/dist/types-qrtt0135.d.mts.map +1 -0
  33. package/dist/update-check-EbNDkzyV.mjs +146 -0
  34. package/dist/update-check-EbNDkzyV.mjs.map +1 -0
  35. package/package.json +61 -20
  36. package/src/args.ts +365 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +312 -0
  40. package/src/cli/index.ts +159 -0
  41. package/src/cli/init.ts +135 -0
  42. package/src/cli/link.ts +320 -0
  43. package/src/cli/wrap.ts +152 -0
  44. package/src/codegen/README.md +118 -0
  45. package/src/codegen/code-builder.ts +226 -0
  46. package/src/codegen/discovery.ts +232 -0
  47. package/src/codegen/file-emitter.ts +73 -0
  48. package/src/codegen/generators/barrel-file.ts +16 -0
  49. package/src/codegen/generators/command-file.ts +184 -0
  50. package/src/codegen/generators/command-tree.ts +124 -0
  51. package/src/codegen/index.ts +33 -0
  52. package/src/codegen/parsers/fish.ts +163 -0
  53. package/src/codegen/parsers/help.ts +378 -0
  54. package/src/codegen/parsers/merge.ts +158 -0
  55. package/src/codegen/parsers/zsh.ts +221 -0
  56. package/src/codegen/schema-to-code.ts +199 -0
  57. package/src/codegen/template.ts +69 -0
  58. package/src/codegen/types.ts +143 -0
  59. package/src/colorizer.ts +2 -2
  60. package/src/command-utils.ts +501 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1044 -284
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +149 -63
  66. package/src/help.ts +151 -55
  67. package/src/index.ts +13 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +31 -16
  70. package/src/repl-loop.ts +317 -0
  71. package/src/runtime.ts +304 -0
  72. package/src/shell-utils.ts +83 -0
  73. package/src/test.ts +285 -0
  74. package/src/type-helpers.ts +12 -12
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +803 -144
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +185 -0
  79. package/src/zod.d.ts +2 -2
  80. package/src/options.ts +0 -180
@@ -0,0 +1,244 @@
1
+ import type { ResolvedPadroneRuntime } from './runtime.ts';
2
+
3
+ /**
4
+ * Configuration for the update check feature.
5
+ */
6
+ export type UpdateCheckConfig = {
7
+ /**
8
+ * The npm package name to check. Defaults to the program name.
9
+ */
10
+ packageName?: string;
11
+ /**
12
+ * Registry to check for updates.
13
+ * - `'npm'` — checks the npm registry (default)
14
+ * - A URL string — custom registry endpoint that returns JSON with a `version` or `dist-tags.latest` field
15
+ */
16
+ registry?: 'npm' | string;
17
+ /**
18
+ * How often to check for updates. Accepts shorthand like `'1d'`, `'12h'`, `'30m'`.
19
+ * Defaults to `'1d'` (once per day).
20
+ */
21
+ interval?: string;
22
+ /**
23
+ * Path to the cache file for storing the last check timestamp and latest version.
24
+ * Defaults to `~/.config/<programName>-update-check.json`.
25
+ */
26
+ cache?: string;
27
+ /**
28
+ * Environment variable name to disable update checks (e.g. `'MYAPP_NO_UPDATE_CHECK'`).
29
+ * When set to a truthy value, update checks are skipped.
30
+ * Defaults to `'<PROGRAM_NAME>_NO_UPDATE_CHECK'` (uppercased, hyphens to underscores).
31
+ */
32
+ disableEnvVar?: string;
33
+ };
34
+
35
+ type CacheData = {
36
+ lastCheck: number;
37
+ latestVersion: string;
38
+ };
39
+
40
+ /**
41
+ * Parses an interval string like '1d', '12h', '30m', '1w' into milliseconds.
42
+ */
43
+ export function parseInterval(interval: string): number {
44
+ const match = interval.match(/^(\d+)\s*(ms|s|m|h|d|w)$/);
45
+ if (!match) return 86_400_000; // default 1d
46
+
47
+ const value = parseInt(match[1]!, 10);
48
+ const unit = match[2]!;
49
+
50
+ switch (unit) {
51
+ case 'ms':
52
+ return value;
53
+ case 's':
54
+ return value * 1000;
55
+ case 'm':
56
+ return value * 60_000;
57
+ case 'h':
58
+ return value * 3_600_000;
59
+ case 'd':
60
+ return value * 86_400_000;
61
+ case 'w':
62
+ return value * 604_800_000;
63
+ default:
64
+ return 86_400_000;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Compares two semver version strings.
70
+ * Returns true if `latest` is newer than `current`.
71
+ */
72
+ export function isNewerVersion(current: string, latest: string): boolean {
73
+ const parse = (v: string) => {
74
+ const cleaned = v.replace(/^v/, '');
75
+ const parts = cleaned.split('-');
76
+ const nums = parts[0]!.split('.').map(Number);
77
+ return { major: nums[0] ?? 0, minor: nums[1] ?? 0, patch: nums[2] ?? 0, prerelease: parts[1] };
78
+ };
79
+
80
+ const c = parse(current);
81
+ const l = parse(latest);
82
+
83
+ // Don't notify about pre-release versions unless user is already on a pre-release
84
+ if (l.prerelease && !c.prerelease) return false;
85
+
86
+ if (l.major !== c.major) return l.major > c.major;
87
+ if (l.minor !== c.minor) return l.minor > c.minor;
88
+ if (l.patch !== c.patch) return l.patch > c.patch;
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Reads the update check cache file.
94
+ */
95
+ function readCache(cachePath: string): CacheData | undefined {
96
+ try {
97
+ const { existsSync, readFileSync } = require('node:fs') as typeof import('node:fs');
98
+ if (!existsSync(cachePath)) return undefined;
99
+ const data = JSON.parse(readFileSync(cachePath, 'utf-8'));
100
+ if (typeof data.lastCheck === 'number' && typeof data.latestVersion === 'string') {
101
+ return data as CacheData;
102
+ }
103
+ } catch {
104
+ // Ignore errors
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ /**
110
+ * Writes the update check cache file.
111
+ */
112
+ function writeCache(cachePath: string, data: CacheData): void {
113
+ try {
114
+ const { existsSync, mkdirSync, writeFileSync } = require('node:fs') as typeof import('node:fs');
115
+ const { dirname } = require('node:path') as typeof import('node:path');
116
+ const dir = dirname(cachePath);
117
+ if (!existsSync(dir)) {
118
+ mkdirSync(dir, { recursive: true });
119
+ }
120
+ writeFileSync(cachePath, JSON.stringify(data), 'utf-8');
121
+ } catch {
122
+ // Ignore errors — cache is best-effort
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Resolves the cache path, expanding `~` to the home directory.
128
+ */
129
+ function resolveCachePath(cachePath: string): string {
130
+ const { homedir } = require('node:os') as typeof import('node:os');
131
+ const { resolve } = require('node:path') as typeof import('node:path');
132
+ if (cachePath.startsWith('~')) {
133
+ return cachePath.replace('~', homedir());
134
+ }
135
+ return resolve(cachePath);
136
+ }
137
+
138
+ /**
139
+ * Fetches the latest version from the registry.
140
+ */
141
+ async function fetchLatestVersion(packageName: string, registry: string): Promise<string | undefined> {
142
+ const url = registry === 'npm' ? `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest` : registry;
143
+
144
+ try {
145
+ const response = await fetch(url);
146
+ if (!response.ok) return undefined;
147
+ const data = (await response.json()) as Record<string, unknown>;
148
+
149
+ // npm registry returns { version: "x.y.z" }
150
+ if (typeof data.version === 'string') return data.version;
151
+
152
+ // Custom endpoint may return { "dist-tags": { latest: "x.y.z" } }
153
+ const distTags = data['dist-tags'] as Record<string, string> | undefined;
154
+ if (distTags?.latest) return distTags.latest;
155
+ } catch {
156
+ // Network errors are expected (offline, firewall, etc.)
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ /**
162
+ * Formats the update notification message.
163
+ */
164
+ export function formatUpdateMessage(currentVersion: string, latestVersion: string, packageName: string): string {
165
+ const updateCommand = `npm update -g ${packageName}`;
166
+ return `\n Update available: ${currentVersion} \u2192 ${latestVersion}\n Run "${updateCommand}" to update\n`;
167
+ }
168
+
169
+ /**
170
+ * Checks for updates in the background. Returns a function that, when called,
171
+ * prints the update notification if a newer version was found.
172
+ *
173
+ * This is designed to be non-blocking: the check starts immediately but the
174
+ * result is only consumed after command execution completes.
175
+ */
176
+ export function createUpdateChecker(
177
+ programName: string,
178
+ currentVersion: string,
179
+ config: UpdateCheckConfig,
180
+ runtime: ResolvedPadroneRuntime,
181
+ ): () => void {
182
+ const packageName = config.packageName ?? programName;
183
+ const registry = config.registry ?? 'npm';
184
+ const intervalMs = parseInterval(config.interval ?? '1d');
185
+ const disableEnvVar = config.disableEnvVar ?? `${programName.toUpperCase().replace(/-/g, '_')}_NO_UPDATE_CHECK`;
186
+
187
+ const defaultCachePath = `~/.config/${programName}-update-check.json`;
188
+ const cachePath = resolveCachePath(config.cache ?? defaultCachePath);
189
+
190
+ // Check if disabled
191
+ const env = runtime.env();
192
+ if (env.CI || env.CONTINUOUS_INTEGRATION) return noop;
193
+ if (env[disableEnvVar]) return noop;
194
+ if (typeof process !== 'undefined' && !process.stdout?.isTTY) return noop;
195
+
196
+ // Check cache — if we checked recently, use cached result
197
+ const cached = readCache(cachePath);
198
+ if (cached && Date.now() - cached.lastCheck < intervalMs) {
199
+ // Use cached version for display
200
+ if (isNewerVersion(currentVersion, cached.latestVersion)) {
201
+ return () => {
202
+ runtime.error(formatUpdateMessage(currentVersion, cached.latestVersion, packageName));
203
+ };
204
+ }
205
+ return noop;
206
+ }
207
+
208
+ // Start background fetch
209
+ const fetchPromise = fetchLatestVersion(packageName, registry).then((latestVersion) => {
210
+ if (latestVersion) {
211
+ writeCache(cachePath, { lastCheck: Date.now(), latestVersion });
212
+ if (isNewerVersion(currentVersion, latestVersion)) {
213
+ return latestVersion;
214
+ }
215
+ }
216
+ return undefined;
217
+ });
218
+
219
+ // Return a function that blocks on the result (briefly — the fetch should be done by now)
220
+ let resolved: string | undefined | null = null; // null = not yet resolved
221
+ fetchPromise.then(
222
+ (v) => {
223
+ resolved = v;
224
+ },
225
+ () => {
226
+ resolved = undefined;
227
+ },
228
+ );
229
+
230
+ return () => {
231
+ // If the fetch already resolved, use the result synchronously
232
+ if (resolved !== null) {
233
+ if (resolved) {
234
+ runtime.error(formatUpdateMessage(currentVersion, resolved, packageName));
235
+ }
236
+ return;
237
+ }
238
+
239
+ // Otherwise, we can't block — just skip this time.
240
+ // The cache will be written when the promise resolves, so next invocation will show the message.
241
+ };
242
+ }
243
+
244
+ function noop() {}
package/src/wrap.ts ADDED
@@ -0,0 +1,185 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { ValidationError } from './errors.ts';
3
+ import type { PadroneSchema } from './types.ts';
4
+
5
+ /**
6
+ * Configuration for wrapping an external CLI tool.
7
+ */
8
+ export type WrapConfig<TCommandArgs extends PadroneSchema = PadroneSchema, TWrapArgs extends PadroneSchema = TCommandArgs> = {
9
+ /**
10
+ * The command to execute (e.g., 'git', 'docker', 'npm').
11
+ */
12
+ command: string;
13
+ /**
14
+ * Optional fixed arguments that always precede the arguments (e.g., ['commit'] for 'git commit').
15
+ */
16
+ args?: string[];
17
+ /**
18
+ * Positional argument configuration for the external command.
19
+ * If not provided, defaults to the wrapping command's positional configuration.
20
+ */
21
+ positional?: string[];
22
+ /**
23
+ * Whether to inherit stdio streams (stdin, stdout, stderr) from the parent process.
24
+ * Default: true
25
+ */
26
+ inheritStdio?: boolean;
27
+ /**
28
+ * Optional schema that transforms command arguments to external CLI arguments.
29
+ * The schema's input type should match the command arguments, and its output type defines
30
+ * the arguments expected by the external command.
31
+ * If not provided, command arguments are passed through as-is.
32
+ */
33
+ schema?: TWrapArgs | ((commandArguments: TCommandArgs) => TWrapArgs);
34
+ };
35
+
36
+ /**
37
+ * Result from executing a wrapped CLI tool.
38
+ */
39
+ export type WrapResult = {
40
+ /**
41
+ * The exit code of the process.
42
+ */
43
+ exitCode: number;
44
+ /**
45
+ * Standard output from the process (only if inheritStdio is false).
46
+ */
47
+ stdout?: string;
48
+ /**
49
+ * Standard error from the process (only if inheritStdio is false).
50
+ */
51
+ stderr?: string;
52
+ /**
53
+ * Whether the process exited successfully (exit code 0).
54
+ */
55
+ success: boolean;
56
+ };
57
+
58
+ /**
59
+ * Converts parsed arguments to CLI arguments for an external command.
60
+ */
61
+ function argsToCliArgs(input: Record<string, unknown> | undefined, positional: string[] = []): string[] {
62
+ const args: string[] = [];
63
+
64
+ // Handle undefined or null input
65
+ if (!input) return args;
66
+
67
+ const positionalValues: Record<string, unknown> = {};
68
+ const regularArguments: Record<string, unknown> = {};
69
+
70
+ // Separate positional and regular arguments
71
+ for (const [key, value] of Object.entries(input)) {
72
+ if (positional.includes(key) || positional.includes(`...${key}`)) {
73
+ positionalValues[key] = value;
74
+ } else {
75
+ regularArguments[key] = value;
76
+ }
77
+ }
78
+
79
+ // Add regular arguments first
80
+ for (const [key, value] of Object.entries(regularArguments)) {
81
+ if (value === undefined || value === null) continue;
82
+
83
+ // Use the key as-is with -- prefix
84
+ const flag = `--${key}`;
85
+
86
+ if (typeof value === 'boolean') {
87
+ if (value) args.push(flag);
88
+ } else if (Array.isArray(value)) {
89
+ // For arrays, add the flag multiple times
90
+ for (const item of value) {
91
+ args.push(flag, String(item));
92
+ }
93
+ } else {
94
+ args.push(flag, String(value));
95
+ }
96
+ }
97
+
98
+ // Add positional arguments in the specified order
99
+ for (const posKey of positional) {
100
+ const isVariadic = posKey.startsWith('...');
101
+ const key = isVariadic ? posKey.slice(3) : posKey;
102
+ const value = positionalValues[key];
103
+
104
+ if (value === undefined || value === null) continue;
105
+
106
+ if (isVariadic && Array.isArray(value)) {
107
+ args.push(...value.map(String));
108
+ } else {
109
+ args.push(String(value));
110
+ }
111
+ }
112
+
113
+ return args;
114
+ }
115
+
116
+ /**
117
+ * Creates an action handler that wraps an external CLI tool.
118
+ * @param config - Configuration for wrapping the external command (includes optional schema)
119
+ * @param commandArguments - The command's arguments schema
120
+ * @param commandPositional - Default positional config from the wrapping command
121
+ */
122
+ export function createWrapHandler<TCommandArgs extends PadroneSchema, TWrapArgs extends PadroneSchema>(
123
+ config: WrapConfig<TCommandArgs, TWrapArgs>,
124
+ commandArguments: TCommandArgs,
125
+ commandPositional?: string[],
126
+ ): (args: StandardSchemaV1.InferOutput<TCommandArgs>) => Promise<WrapResult> {
127
+ return async (args: StandardSchemaV1.InferOutput<TCommandArgs>): Promise<WrapResult> => {
128
+ const { command, args: fixedArgs = [], inheritStdio = true, positional = commandPositional, schema: wrapSchema } = config;
129
+
130
+ // Get the wrap schema (handle function or direct schema)
131
+ const schema = wrapSchema ? (typeof wrapSchema === 'function' ? wrapSchema(commandArguments) : wrapSchema) : commandArguments;
132
+
133
+ // Transform command arguments to external CLI arguments using the wrap schema
134
+ const validationResult = schema['~standard'].validate(args);
135
+
136
+ const processResult = (result: StandardSchemaV1.Result<unknown>) => {
137
+ if (result.issues) {
138
+ const issueMessages = result.issues
139
+ .map((i: StandardSchemaV1.Issue) => ` - ${(i.path as (string | number)[] | undefined)?.join('.') || 'root'}: ${i.message}`)
140
+ .join('\n');
141
+ throw new ValidationError(`Wrap schema validation failed:\n${issueMessages}`, result.issues as any);
142
+ }
143
+ return result.value;
144
+ };
145
+
146
+ const externalArguments =
147
+ validationResult instanceof Promise ? await validationResult.then(processResult) : processResult(validationResult);
148
+
149
+ // Convert arguments to CLI arguments
150
+ const regularArgs = argsToCliArgs(externalArguments as Record<string, unknown>, positional);
151
+
152
+ // Combine fixed args and regular args
153
+ const allArgs = [...fixedArgs, ...regularArgs];
154
+
155
+ // Execute the external command
156
+ const proc = Bun.spawn([command, ...allArgs], {
157
+ stdout: inheritStdio ? 'inherit' : 'pipe',
158
+ stderr: inheritStdio ? 'inherit' : 'pipe',
159
+ stdin: inheritStdio ? 'inherit' : 'ignore',
160
+ });
161
+
162
+ const exitCode = await proc.exited;
163
+
164
+ let stdout: string | undefined;
165
+ let stderr: string | undefined;
166
+
167
+ if (!inheritStdio) {
168
+ if (proc.stdout) {
169
+ const stdoutBuffer = await new Response(proc.stdout).arrayBuffer();
170
+ stdout = new TextDecoder().decode(stdoutBuffer);
171
+ }
172
+ if (proc.stderr) {
173
+ const stderrBuffer = await new Response(proc.stderr).arrayBuffer();
174
+ stderr = new TextDecoder().decode(stderrBuffer);
175
+ }
176
+ }
177
+
178
+ return {
179
+ exitCode,
180
+ stdout,
181
+ stderr,
182
+ success: exitCode === 0,
183
+ };
184
+ };
185
+ }
package/src/zod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { PadroneOptionsMeta } from './options.ts';
1
+ import type { PadroneFieldMeta } from './args.ts';
2
2
 
3
3
  declare module 'zod/v4/core' {
4
- export interface GlobalMeta extends PadroneOptionsMeta {}
4
+ export interface GlobalMeta extends PadroneFieldMeta {}
5
5
  }
package/src/options.ts DELETED
@@ -1,180 +0,0 @@
1
- import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
2
-
3
- export interface PadroneOptionsMeta {
4
- description?: string;
5
- alias?: string[] | string;
6
- deprecated?: boolean | string;
7
- hidden?: boolean;
8
- examples?: unknown[];
9
- }
10
-
11
- type PositionalArgs<TObj> =
12
- TObj extends Record<string, any>
13
- ? {
14
- [K in keyof TObj]: TObj[K] extends Array<any> ? `...${K & string}` : K & string;
15
- }[keyof TObj]
16
- : string;
17
-
18
- /**
19
- * Meta configuration for options including positional arguments.
20
- * The `positional` array defines which options are positional arguments and their order.
21
- * Use '...name' prefix to indicate variadic (rest) arguments, matching JS/TS rest syntax.
22
- *
23
- * @example
24
- * ```ts
25
- * .options(schema, {
26
- * positional: ['source', '...files', 'dest'], // '...files' is variadic
27
- * })
28
- * ```
29
- */
30
- export interface PadroneMeta<TObj = Record<string, any>> {
31
- /**
32
- * Array of option names that should be treated as positional arguments.
33
- * Order in array determines position. Use '...name' prefix for variadic args.
34
- * @example ['source', '...files', 'dest'] - 'files' captures multiple values
35
- */
36
- positional?: PositionalArgs<TObj>[];
37
- /**
38
- * Per-option metadata.
39
- */
40
- options?: { [K in keyof TObj]?: PadroneOptionsMeta };
41
- }
42
-
43
- /**
44
- * Parse positional configuration to extract names and variadic info.
45
- */
46
- export function parsePositionalConfig(positional: string[]): { name: string; variadic: boolean }[] {
47
- return positional.map((p) => {
48
- const isVariadic = p.startsWith('...');
49
- const name = isVariadic ? p.slice(3) : p;
50
- return { name, variadic: isVariadic };
51
- });
52
- }
53
-
54
- /**
55
- * Result type for extractSchemaMetadata function.
56
- */
57
- interface SchemaMetadataResult {
58
- aliases: Record<string, string>;
59
- }
60
-
61
- /**
62
- * Extract all option metadata from schema and meta in a single pass.
63
- * This consolidates aliases, env bindings, and config keys extraction.
64
- */
65
- export function extractSchemaMetadata(
66
- schema: StandardJSONSchemaV1,
67
- meta?: Record<string, PadroneOptionsMeta | undefined>,
68
- ): SchemaMetadataResult {
69
- const aliases: Record<string, string> = {};
70
-
71
- // Extract from meta object
72
- if (meta) {
73
- for (const [key, value] of Object.entries(meta)) {
74
- if (!value) continue;
75
-
76
- // Extract aliases
77
- if (value.alias) {
78
- const list = typeof value.alias === 'string' ? [value.alias] : value.alias;
79
- for (const aliasKey of list) {
80
- if (typeof aliasKey === 'string' && aliasKey && aliasKey !== key) {
81
- aliases[aliasKey] = key;
82
- }
83
- }
84
- }
85
- }
86
- }
87
-
88
- // Extract from JSON schema properties
89
- try {
90
- const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
91
- if (jsonSchema.type === 'object' && jsonSchema.properties) {
92
- for (const [propertyName, propertySchema] of Object.entries(jsonSchema.properties as Record<string, any>)) {
93
- if (!propertySchema) continue;
94
-
95
- // Extract aliases from schema
96
- const propAlias = propertySchema.alias;
97
- if (propAlias) {
98
- const list = typeof propAlias === 'string' ? [propAlias] : propAlias;
99
- if (Array.isArray(list)) {
100
- for (const aliasKey of list) {
101
- if (typeof aliasKey === 'string' && aliasKey && aliasKey !== propertyName && !(aliasKey in aliases)) {
102
- aliases[aliasKey] = propertyName;
103
- }
104
- }
105
- }
106
- }
107
- }
108
- }
109
- } catch {
110
- // Ignore errors from JSON schema generation
111
- }
112
-
113
- return { aliases };
114
- }
115
-
116
- function preprocessAliases(data: Record<string, unknown>, aliases: Record<string, string>): Record<string, unknown> {
117
- const result = { ...data };
118
-
119
- for (const [aliasKey, fullOptionName] of Object.entries(aliases)) {
120
- if (aliasKey in data && aliasKey !== fullOptionName) {
121
- const aliasValue = data[aliasKey];
122
- // Prefer full option name if it exists
123
- if (!(fullOptionName in result)) result[fullOptionName] = aliasValue;
124
- delete result[aliasKey];
125
- }
126
- }
127
-
128
- return result;
129
- }
130
-
131
- interface ParseOptionsContext {
132
- aliases?: Record<string, string>;
133
- envData?: Record<string, unknown>;
134
- configData?: Record<string, unknown>;
135
- }
136
-
137
- /**
138
- * Apply values directly to options.
139
- * CLI values take precedence over the provided values.
140
- */
141
- function applyValues(data: Record<string, unknown>, values: Record<string, unknown>): Record<string, unknown> {
142
- const result = { ...data };
143
-
144
- for (const [key, value] of Object.entries(values)) {
145
- // Only apply value if option wasn't already set
146
- if (key in result && result[key] !== undefined) continue;
147
- if (value !== undefined) {
148
- result[key] = value;
149
- }
150
- }
151
-
152
- return result;
153
- }
154
-
155
- /**
156
- * Combined preprocessing of options with all features.
157
- * Precedence order (highest to lowest): CLI args > env vars > config file
158
- */
159
- export function preprocessOptions(data: Record<string, unknown>, ctx: ParseOptionsContext): Record<string, unknown> {
160
- let result = { ...data };
161
-
162
- // 1. Apply aliases first
163
- if (ctx.aliases && Object.keys(ctx.aliases).length > 0) {
164
- result = preprocessAliases(result, ctx.aliases);
165
- }
166
-
167
- // 2. Apply environment variables (higher precedence than config)
168
- // These only apply if CLI didn't set the option
169
- if (ctx.envData) {
170
- result = applyValues(result, ctx.envData);
171
- }
172
-
173
- // 3. Apply config file values (lowest precedence)
174
- // These only apply if neither CLI nor env set the option
175
- if (ctx.configData) {
176
- result = applyValues(result, ctx.configData);
177
- }
178
-
179
- return result;
180
- }