padrone 1.1.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 +38 -1
  2. package/LICENSE +1 -1
  3. package/README.md +60 -30
  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 +120 -546
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1180 -1197
  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 -21
  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 +1036 -305
  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 +12 -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 +10 -10
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +752 -154
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +44 -40
  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 CHANGED
@@ -1,16 +1,17 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { ValidationError } from './errors.ts';
2
3
  import type { PadroneSchema } from './types.ts';
3
4
 
4
5
  /**
5
- * Configuration options for wrapping an external CLI tool.
6
+ * Configuration for wrapping an external CLI tool.
6
7
  */
7
- export type WrapConfig<TCommandOpts extends PadroneSchema = PadroneSchema, TWrapOpts extends PadroneSchema = TCommandOpts> = {
8
+ export type WrapConfig<TCommandArgs extends PadroneSchema = PadroneSchema, TWrapArgs extends PadroneSchema = TCommandArgs> = {
8
9
  /**
9
10
  * The command to execute (e.g., 'git', 'docker', 'npm').
10
11
  */
11
12
  command: string;
12
13
  /**
13
- * Optional fixed arguments that always precede the options (e.g., ['commit'] for 'git commit').
14
+ * Optional fixed arguments that always precede the arguments (e.g., ['commit'] for 'git commit').
14
15
  */
15
16
  args?: string[];
16
17
  /**
@@ -24,12 +25,12 @@ export type WrapConfig<TCommandOpts extends PadroneSchema = PadroneSchema, TWrap
24
25
  */
25
26
  inheritStdio?: boolean;
26
27
  /**
27
- * Optional schema that transforms command options to external CLI arguments.
28
- * The schema's input type should match the command options, and its output type defines
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
29
30
  * the arguments expected by the external command.
30
- * If not provided, command options are passed through as-is.
31
+ * If not provided, command arguments are passed through as-is.
31
32
  */
32
- schema?: TWrapOpts | ((commandOptions: TCommandOpts) => TWrapOpts);
33
+ schema?: TWrapArgs | ((commandArguments: TCommandArgs) => TWrapArgs);
33
34
  };
34
35
 
35
36
  /**
@@ -55,28 +56,28 @@ export type WrapResult = {
55
56
  };
56
57
 
57
58
  /**
58
- * Converts parsed options to CLI arguments for an external command.
59
+ * Converts parsed arguments to CLI arguments for an external command.
59
60
  */
60
- function optionsToArgs(options: Record<string, unknown> | undefined, positional: string[] = []): string[] {
61
+ function argsToCliArgs(input: Record<string, unknown> | undefined, positional: string[] = []): string[] {
61
62
  const args: string[] = [];
62
63
 
63
- // Handle undefined or null options
64
- if (!options) return args;
64
+ // Handle undefined or null input
65
+ if (!input) return args;
65
66
 
66
67
  const positionalValues: Record<string, unknown> = {};
67
- const regularOptions: Record<string, unknown> = {};
68
+ const regularArguments: Record<string, unknown> = {};
68
69
 
69
- // Separate positional and regular options
70
- for (const [key, value] of Object.entries(options)) {
70
+ // Separate positional and regular arguments
71
+ for (const [key, value] of Object.entries(input)) {
71
72
  if (positional.includes(key) || positional.includes(`...${key}`)) {
72
73
  positionalValues[key] = value;
73
74
  } else {
74
- regularOptions[key] = value;
75
+ regularArguments[key] = value;
75
76
  }
76
77
  }
77
78
 
78
- // Add regular options first
79
- for (const [key, value] of Object.entries(regularOptions)) {
79
+ // Add regular arguments first
80
+ for (const [key, value] of Object.entries(regularArguments)) {
80
81
  if (value === undefined || value === null) continue;
81
82
 
82
83
  // Use the key as-is with -- prefix
@@ -115,38 +116,41 @@ function optionsToArgs(options: Record<string, unknown> | undefined, positional:
115
116
  /**
116
117
  * Creates an action handler that wraps an external CLI tool.
117
118
  * @param config - Configuration for wrapping the external command (includes optional schema)
118
- * @param commandOptions - The command's options schema
119
+ * @param commandArguments - The command's arguments schema
119
120
  * @param commandPositional - Default positional config from the wrapping command
120
121
  */
121
- export function createWrapHandler<TCommandOpts extends PadroneSchema, TWrapOpts extends PadroneSchema>(
122
- config: WrapConfig<TCommandOpts, TWrapOpts>,
123
- commandOptions: TCommandOpts,
122
+ export function createWrapHandler<TCommandArgs extends PadroneSchema, TWrapArgs extends PadroneSchema>(
123
+ config: WrapConfig<TCommandArgs, TWrapArgs>,
124
+ commandArguments: TCommandArgs,
124
125
  commandPositional?: string[],
125
- ): (options: StandardSchemaV1.InferOutput<TCommandOpts>) => Promise<WrapResult> {
126
- return async (options: StandardSchemaV1.InferOutput<TCommandOpts>): Promise<WrapResult> => {
126
+ ): (args: StandardSchemaV1.InferOutput<TCommandArgs>) => Promise<WrapResult> {
127
+ return async (args: StandardSchemaV1.InferOutput<TCommandArgs>): Promise<WrapResult> => {
127
128
  const { command, args: fixedArgs = [], inheritStdio = true, positional = commandPositional, schema: wrapSchema } = config;
128
129
 
129
130
  // Get the wrap schema (handle function or direct schema)
130
- const schema = wrapSchema ? (typeof wrapSchema === 'function' ? wrapSchema(commandOptions) : wrapSchema) : commandOptions;
131
+ const schema = wrapSchema ? (typeof wrapSchema === 'function' ? wrapSchema(commandArguments) : wrapSchema) : commandArguments;
131
132
 
132
- // Transform command options to external CLI options using the wrap schema
133
- const result = schema['~standard'].validate(options);
134
- if (result instanceof Promise) {
135
- throw new Error('Async validation is not supported. Wrap schema validate() must return a synchronous result.');
136
- }
137
- if (result.issues) {
138
- const issueMessages = result.issues
139
- .map((i) => ` - ${(i.path as (string | number)[] | undefined)?.join('.') || 'root'}: ${i.message}`)
140
- .join('\n');
141
- throw new Error(`Wrap schema validation failed:\n${issueMessages}`);
142
- }
143
- const externalOptions = result.value;
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);
144
148
 
145
- // Convert options to CLI arguments
146
- const optionArgs = optionsToArgs(externalOptions as Record<string, unknown>, positional);
149
+ // Convert arguments to CLI arguments
150
+ const regularArgs = argsToCliArgs(externalArguments as Record<string, unknown>, positional);
147
151
 
148
- // Combine fixed args and option args
149
- const allArgs = [...fixedArgs, ...optionArgs];
152
+ // Combine fixed args and regular args
153
+ const allArgs = [...fixedArgs, ...regularArgs];
150
154
 
151
155
  // Execute the external command
152
156
  const proc = Bun.spawn([command, ...allArgs], {
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
- * .arguments(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
- }