padrone 1.0.0-beta.2
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.
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/index.d.mts +384 -0
- package/index.mjs +1246 -0
- package/index.mjs.map +1 -0
- package/package.json +64 -0
- package/src/colorizer.ts +41 -0
- package/src/create.ts +559 -0
- package/src/formatter.ts +499 -0
- package/src/help.ts +227 -0
- package/src/index.ts +22 -0
- package/src/options.ts +290 -0
- package/src/parse.ts +222 -0
- package/src/type-utils.ts +99 -0
- package/src/types.ts +313 -0
- package/src/utils.ts +131 -0
- package/src/zod.d.ts +5 -0
package/src/help.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { createFormatter, type HelpArgumentInfo, type HelpDetail, type HelpFormat, type HelpInfo, type HelpOptionInfo } from './formatter';
|
|
3
|
+
import { extractSchemaMetadata, type PadroneMeta, parsePositionalConfig } from './options';
|
|
4
|
+
import type { AnyPadroneCommand } from './types';
|
|
5
|
+
import { getRootCommand } from './utils';
|
|
6
|
+
|
|
7
|
+
export type HelpOptions = {
|
|
8
|
+
format?: HelpFormat | 'auto';
|
|
9
|
+
detail?: HelpDetail;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract positional arguments info from schema based on meta.positional config.
|
|
14
|
+
*/
|
|
15
|
+
function extractPositionalArgsInfo(
|
|
16
|
+
schema: StandardJSONSchemaV1,
|
|
17
|
+
meta?: PadroneMeta,
|
|
18
|
+
): { args: HelpArgumentInfo[]; positionalNames: Set<string> } {
|
|
19
|
+
const args: HelpArgumentInfo[] = [];
|
|
20
|
+
const positionalNames = new Set<string>();
|
|
21
|
+
|
|
22
|
+
if (!schema || !meta?.positional || meta.positional.length === 0) {
|
|
23
|
+
return { args, positionalNames };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const positionalConfig = parsePositionalConfig(meta.positional);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
30
|
+
|
|
31
|
+
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
32
|
+
const properties = jsonSchema.properties as Record<string, any>;
|
|
33
|
+
const required = (jsonSchema.required as string[]) || [];
|
|
34
|
+
|
|
35
|
+
for (const { name, variadic } of positionalConfig) {
|
|
36
|
+
const prop = properties[name];
|
|
37
|
+
if (!prop) continue;
|
|
38
|
+
|
|
39
|
+
positionalNames.add(name);
|
|
40
|
+
const optMeta = meta.options?.[name];
|
|
41
|
+
|
|
42
|
+
args.push({
|
|
43
|
+
name: variadic ? `...${name}` : name,
|
|
44
|
+
description: optMeta?.description ?? prop.description,
|
|
45
|
+
optional: !required.includes(name),
|
|
46
|
+
default: prop.default,
|
|
47
|
+
type: variadic ? `array<${prop.items?.type || 'string'}>` : prop.type,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Fallback to empty result if toJSONSchema fails
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { args, positionalNames };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, positionalNames?: Set<string>) {
|
|
59
|
+
const result: HelpOptionInfo[] = [];
|
|
60
|
+
if (!schema) return result;
|
|
61
|
+
|
|
62
|
+
const vendor = schema['~standard'].vendor;
|
|
63
|
+
if (!vendor.includes('zod')) return result;
|
|
64
|
+
|
|
65
|
+
const optionsMeta = meta?.options;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
69
|
+
|
|
70
|
+
// Handle object: z.object({ key: z.string(), ... })
|
|
71
|
+
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
72
|
+
const properties = jsonSchema.properties as Record<string, any>;
|
|
73
|
+
const required = (jsonSchema.required as string[]) || [];
|
|
74
|
+
const propertyNames = new Set(Object.keys(properties));
|
|
75
|
+
|
|
76
|
+
// Helper to check if a negated version of an option exists
|
|
77
|
+
const hasExplicitNegation = (key: string): boolean => {
|
|
78
|
+
// Check for noVerbose style (camelCase)
|
|
79
|
+
const camelNegated = `no${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
80
|
+
if (propertyNames.has(camelNegated)) return true;
|
|
81
|
+
// Check for no-verbose style (kebab-case, though rare in JS)
|
|
82
|
+
const kebabNegated = `no-${key}`;
|
|
83
|
+
if (propertyNames.has(kebabNegated)) return true;
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Helper to check if this option is itself a negation of another option
|
|
88
|
+
const isNegationOf = (key: string): boolean => {
|
|
89
|
+
// Check for noVerbose -> verbose (camelCase)
|
|
90
|
+
if (key.startsWith('no') && key.length > 2 && key[2] === key[2]?.toUpperCase()) {
|
|
91
|
+
const positiveKey = key.charAt(2).toLowerCase() + key.slice(3);
|
|
92
|
+
if (propertyNames.has(positiveKey)) return true;
|
|
93
|
+
}
|
|
94
|
+
// Check for no-verbose -> verbose (kebab-case)
|
|
95
|
+
if (key.startsWith('no-')) {
|
|
96
|
+
const positiveKey = key.slice(3);
|
|
97
|
+
if (propertyNames.has(positiveKey)) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
103
|
+
// Skip positional arguments - they are shown in arguments section
|
|
104
|
+
if (positionalNames?.has(key)) continue;
|
|
105
|
+
|
|
106
|
+
const isOptional = !required.includes(key);
|
|
107
|
+
const enumValues = prop.enum as string[] | undefined;
|
|
108
|
+
const optMeta = optionsMeta?.[key];
|
|
109
|
+
const propType = prop.type as string;
|
|
110
|
+
|
|
111
|
+
// Booleans are negatable unless there's an explicit noOption property
|
|
112
|
+
// or this option is itself a negation of another option
|
|
113
|
+
const isNegatable = propType === 'boolean' && !hasExplicitNegation(key) && !isNegationOf(key);
|
|
114
|
+
|
|
115
|
+
result.push({
|
|
116
|
+
name: key,
|
|
117
|
+
description: optMeta?.description ?? prop.description,
|
|
118
|
+
optional: isOptional,
|
|
119
|
+
default: prop.default,
|
|
120
|
+
type: propType,
|
|
121
|
+
enum: enumValues,
|
|
122
|
+
deprecated: optMeta?.deprecated ?? prop?.deprecated,
|
|
123
|
+
hidden: optMeta?.hidden ?? prop?.hidden,
|
|
124
|
+
examples: optMeta?.examples ?? prop?.examples,
|
|
125
|
+
env: optMeta?.env ?? prop?.env,
|
|
126
|
+
variadic: propType === 'array', // Arrays are always variadic
|
|
127
|
+
negatable: isNegatable,
|
|
128
|
+
configKey: optMeta?.configKey ?? prop?.configKey,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Fallback to empty result if toJSONSchema fails
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Core Help Info Builder
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds a comprehensive HelpInfo structure from a command.
|
|
145
|
+
* This is the single source of truth that all formatters use.
|
|
146
|
+
* @param cmd - The command to build help info for
|
|
147
|
+
* @param detail - The level of detail ('minimal', 'standard', or 'full')
|
|
148
|
+
*/
|
|
149
|
+
function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpOptions['detail'] = 'standard'): HelpInfo {
|
|
150
|
+
const rootCmd = getRootCommand(cmd);
|
|
151
|
+
const commandName = cmd.path || cmd.name || 'program';
|
|
152
|
+
|
|
153
|
+
// Extract positional args from options schema based on meta.positional
|
|
154
|
+
const { args: positionalArgs, positionalNames } = cmd.options
|
|
155
|
+
? extractPositionalArgsInfo(cmd.options, cmd.meta)
|
|
156
|
+
: { args: [], positionalNames: new Set<string>() };
|
|
157
|
+
|
|
158
|
+
const hasArguments = positionalArgs.length > 0;
|
|
159
|
+
|
|
160
|
+
const helpInfo: HelpInfo = {
|
|
161
|
+
name: commandName,
|
|
162
|
+
title: cmd.title,
|
|
163
|
+
description: cmd.description,
|
|
164
|
+
deprecated: cmd.deprecated,
|
|
165
|
+
hidden: cmd.hidden,
|
|
166
|
+
usage: {
|
|
167
|
+
command: rootCmd === cmd ? commandName : `${rootCmd.name} ${commandName}`,
|
|
168
|
+
hasSubcommands: !!(cmd.commands && cmd.commands.length > 0),
|
|
169
|
+
hasArguments,
|
|
170
|
+
hasOptions: !!cmd.options,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Build subcommands info (filter out hidden commands unless showing full detail)
|
|
175
|
+
if (cmd.commands && cmd.commands.length > 0) {
|
|
176
|
+
const visibleCommands = detail === 'full' ? cmd.commands : cmd.commands.filter((c) => !c.hidden);
|
|
177
|
+
helpInfo.subcommands = visibleCommands.map((c) => ({
|
|
178
|
+
name: c.name,
|
|
179
|
+
title: c.title,
|
|
180
|
+
description: c.description,
|
|
181
|
+
deprecated: c.deprecated,
|
|
182
|
+
hidden: c.hidden,
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
// In 'full' detail mode, recursively build help for all nested commands
|
|
186
|
+
if (detail === 'full') {
|
|
187
|
+
helpInfo.nestedCommands = visibleCommands.map((c) => getHelpInfo(c, 'full'));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build arguments info from positional options
|
|
192
|
+
if (hasArguments) {
|
|
193
|
+
helpInfo.arguments = positionalArgs;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build options info with aliases (excluding positional args)
|
|
197
|
+
if (cmd.options) {
|
|
198
|
+
const optionsInfo = extractOptionsInfo(cmd.options, cmd.meta, positionalNames);
|
|
199
|
+
const optMap: Record<string, HelpOptionInfo> = Object.fromEntries(optionsInfo.map((opt) => [opt.name, opt]));
|
|
200
|
+
|
|
201
|
+
// Merge aliases into options
|
|
202
|
+
const { aliases } = extractSchemaMetadata(cmd.options, cmd.meta?.options);
|
|
203
|
+
for (const [alias, name] of Object.entries(aliases)) {
|
|
204
|
+
const opt = optMap[name];
|
|
205
|
+
if (!opt) continue;
|
|
206
|
+
opt.aliases = [...(opt.aliases || []), alias];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Filter out hidden options
|
|
210
|
+
const visibleOptions = optionsInfo.filter((opt) => !opt.hidden);
|
|
211
|
+
if (visibleOptions.length > 0) {
|
|
212
|
+
helpInfo.options = visibleOptions;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return helpInfo;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Main Entry Point
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, options?: HelpOptions): string {
|
|
224
|
+
const helpInfo = getHelpInfo(commandObj, options?.detail);
|
|
225
|
+
const formatter = createFormatter(options?.format ?? 'auto', options?.detail);
|
|
226
|
+
return formatter.format(helpInfo);
|
|
227
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createPadroneCommandBuilder } from './create';
|
|
2
|
+
import type { PadroneCommand, PadroneProgram } from './types';
|
|
3
|
+
|
|
4
|
+
export function createPadrone(name: string): PadroneProgram {
|
|
5
|
+
return createPadroneCommandBuilder({ name, path: '', commands: [] } as PadroneCommand) as PadroneProgram;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type { HelpArgumentInfo, HelpFormat, HelpInfo, HelpOptionInfo, HelpSubcommandInfo } from './formatter';
|
|
9
|
+
export type { HelpOptions } from './help';
|
|
10
|
+
export type { PadroneOptionsMeta } from './options';
|
|
11
|
+
// Re-export types for consumers
|
|
12
|
+
export type {
|
|
13
|
+
AnyPadroneCommand,
|
|
14
|
+
AnyPadroneProgram,
|
|
15
|
+
PadroneCommand,
|
|
16
|
+
PadroneCommandBuilder,
|
|
17
|
+
PadroneCommandConfig,
|
|
18
|
+
PadroneCommandResult,
|
|
19
|
+
PadroneParseOptions,
|
|
20
|
+
PadroneParseResult,
|
|
21
|
+
PadroneProgram,
|
|
22
|
+
} from './types';
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
* Environment variable name(s) to bind this option to.
|
|
11
|
+
* Can be a single string or array of env var names (checked in order).
|
|
12
|
+
*/
|
|
13
|
+
env?: string | string[];
|
|
14
|
+
/**
|
|
15
|
+
* Key path in config file that maps to this option.
|
|
16
|
+
* Supports dot notation for nested keys (e.g., 'server.port').
|
|
17
|
+
*/
|
|
18
|
+
configKey?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type PositionalArgs<TObj> =
|
|
22
|
+
TObj extends Record<string, any>
|
|
23
|
+
? {
|
|
24
|
+
[K in keyof TObj]: TObj[K] extends Array<any> ? `...${K & string}` : K & string;
|
|
25
|
+
}[keyof TObj]
|
|
26
|
+
: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Meta configuration for options including positional arguments.
|
|
30
|
+
* The `positional` array defines which options are positional arguments and their order.
|
|
31
|
+
* Use '...name' prefix to indicate variadic (rest) arguments, matching JS/TS rest syntax.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* .options(schema, {
|
|
36
|
+
* positional: ['source', '...files', 'dest'], // '...files' is variadic
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export interface PadroneMeta<TObj = Record<string, any>> {
|
|
41
|
+
/**
|
|
42
|
+
* Array of option names that should be treated as positional arguments.
|
|
43
|
+
* Order in array determines position. Use '...name' prefix for variadic args.
|
|
44
|
+
* @example ['source', '...files', 'dest'] - 'files' captures multiple values
|
|
45
|
+
*/
|
|
46
|
+
positional?: PositionalArgs<TObj>[];
|
|
47
|
+
/**
|
|
48
|
+
* Per-option metadata.
|
|
49
|
+
*/
|
|
50
|
+
options?: { [K in keyof TObj]?: PadroneOptionsMeta };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse positional configuration to extract names and variadic info.
|
|
55
|
+
*/
|
|
56
|
+
export function parsePositionalConfig(positional: string[]): { name: string; variadic: boolean }[] {
|
|
57
|
+
return positional.map((p) => {
|
|
58
|
+
const isVariadic = p.startsWith('...');
|
|
59
|
+
const name = isVariadic ? p.slice(3) : p;
|
|
60
|
+
return { name, variadic: isVariadic };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result type for extractSchemaMetadata function.
|
|
66
|
+
*/
|
|
67
|
+
interface SchemaMetadataResult {
|
|
68
|
+
aliases: Record<string, string>;
|
|
69
|
+
envBindings: Record<string, string[]>;
|
|
70
|
+
configKeys: Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract all option metadata from schema and meta in a single pass.
|
|
75
|
+
* This consolidates aliases, env bindings, and config keys extraction.
|
|
76
|
+
*/
|
|
77
|
+
export function extractSchemaMetadata(
|
|
78
|
+
schema: StandardJSONSchemaV1,
|
|
79
|
+
meta?: Record<string, PadroneOptionsMeta | undefined>,
|
|
80
|
+
): SchemaMetadataResult {
|
|
81
|
+
const aliases: Record<string, string> = {};
|
|
82
|
+
const envBindings: Record<string, string[]> = {};
|
|
83
|
+
const configKeys: Record<string, string> = {};
|
|
84
|
+
|
|
85
|
+
// Extract from meta object
|
|
86
|
+
if (meta) {
|
|
87
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
88
|
+
if (!value) continue;
|
|
89
|
+
|
|
90
|
+
// Extract aliases
|
|
91
|
+
if (value.alias) {
|
|
92
|
+
const list = typeof value.alias === 'string' ? [value.alias] : value.alias;
|
|
93
|
+
for (const aliasKey of list) {
|
|
94
|
+
if (typeof aliasKey === 'string' && aliasKey && aliasKey !== key) {
|
|
95
|
+
aliases[aliasKey] = key;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract env bindings
|
|
101
|
+
if (value.env) {
|
|
102
|
+
envBindings[key] = typeof value.env === 'string' ? [value.env] : value.env;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract config keys
|
|
106
|
+
if (value.configKey) {
|
|
107
|
+
configKeys[key] = value.configKey;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract from JSON schema properties
|
|
113
|
+
try {
|
|
114
|
+
const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
115
|
+
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
116
|
+
for (const [propertyName, propertySchema] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
117
|
+
if (!propertySchema) continue;
|
|
118
|
+
|
|
119
|
+
// Extract aliases from schema
|
|
120
|
+
const propAlias = propertySchema.alias;
|
|
121
|
+
if (propAlias) {
|
|
122
|
+
const list = typeof propAlias === 'string' ? [propAlias] : propAlias;
|
|
123
|
+
if (Array.isArray(list)) {
|
|
124
|
+
for (const aliasKey of list) {
|
|
125
|
+
if (typeof aliasKey === 'string' && aliasKey && aliasKey !== propertyName && !(aliasKey in aliases)) {
|
|
126
|
+
aliases[aliasKey] = propertyName;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract env bindings from schema
|
|
133
|
+
if (propertySchema.env && !(propertyName in envBindings)) {
|
|
134
|
+
const envVars = typeof propertySchema.env === 'string' ? [propertySchema.env] : propertySchema.env;
|
|
135
|
+
if (Array.isArray(envVars)) {
|
|
136
|
+
envBindings[propertyName] = envVars;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract config keys from schema
|
|
141
|
+
if (propertySchema.configKey && !(propertyName in configKeys)) {
|
|
142
|
+
configKeys[propertyName] = propertySchema.configKey;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore errors from JSON schema generation
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { aliases, envBindings, configKeys };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function preprocessAliases(data: Record<string, unknown>, aliases: Record<string, string>): Record<string, unknown> {
|
|
154
|
+
const result = { ...data };
|
|
155
|
+
|
|
156
|
+
for (const [aliasKey, fullOptionName] of Object.entries(aliases)) {
|
|
157
|
+
if (aliasKey in data && aliasKey !== fullOptionName) {
|
|
158
|
+
const aliasValue = data[aliasKey];
|
|
159
|
+
// Prefer full option name if it exists
|
|
160
|
+
if (!(fullOptionName in result)) result[fullOptionName] = aliasValue;
|
|
161
|
+
delete result[aliasKey];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Apply environment variable values to options.
|
|
170
|
+
* CLI values take precedence over environment variables.
|
|
171
|
+
*/
|
|
172
|
+
function applyEnvBindings(
|
|
173
|
+
data: Record<string, unknown>,
|
|
174
|
+
envBindings: Record<string, string[]>,
|
|
175
|
+
env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},
|
|
176
|
+
): Record<string, unknown> {
|
|
177
|
+
const result = { ...data };
|
|
178
|
+
|
|
179
|
+
for (const [optionName, envVars] of Object.entries(envBindings)) {
|
|
180
|
+
// Only apply env var if option wasn't already set
|
|
181
|
+
if (optionName in result && result[optionName] !== undefined) continue;
|
|
182
|
+
|
|
183
|
+
for (const envVar of envVars) {
|
|
184
|
+
const envValue = env[envVar];
|
|
185
|
+
if (envValue !== undefined) {
|
|
186
|
+
// Try to parse the value intelligently
|
|
187
|
+
result[optionName] = parseEnvValue(envValue);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parse an environment variable value, attempting to convert to appropriate types.
|
|
198
|
+
*/
|
|
199
|
+
function parseEnvValue(value: string): unknown {
|
|
200
|
+
// Handle boolean-like values
|
|
201
|
+
const lowerValue = value.toLowerCase();
|
|
202
|
+
if (lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes') return true;
|
|
203
|
+
if (lowerValue === 'false' || lowerValue === '0' || lowerValue === 'no') return false;
|
|
204
|
+
|
|
205
|
+
// Handle numeric values
|
|
206
|
+
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
|
|
207
|
+
if (/^-?\d+\.\d+$/.test(value)) return Number.parseFloat(value);
|
|
208
|
+
|
|
209
|
+
// Handle arrays (comma-separated)
|
|
210
|
+
if (value.includes(',')) {
|
|
211
|
+
return value.split(',').map((v) => parseEnvValue(v.trim()));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get a nested value from an object using dot notation.
|
|
219
|
+
*/
|
|
220
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
221
|
+
const parts = path.split('.');
|
|
222
|
+
let current: unknown = obj;
|
|
223
|
+
|
|
224
|
+
for (const part of parts) {
|
|
225
|
+
if (current === null || current === undefined) return undefined;
|
|
226
|
+
if (typeof current !== 'object') return undefined;
|
|
227
|
+
current = (current as Record<string, unknown>)[part];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return current;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Apply config file values to options.
|
|
235
|
+
* CLI values and env values take precedence over config file values.
|
|
236
|
+
*/
|
|
237
|
+
function applyConfigValues(
|
|
238
|
+
data: Record<string, unknown>,
|
|
239
|
+
configKeys: Record<string, string>,
|
|
240
|
+
configData: Record<string, unknown>,
|
|
241
|
+
): Record<string, unknown> {
|
|
242
|
+
const result = { ...data };
|
|
243
|
+
|
|
244
|
+
for (const [optionName, configKey] of Object.entries(configKeys)) {
|
|
245
|
+
// Only apply config value if option wasn't already set
|
|
246
|
+
if (optionName in result && result[optionName] !== undefined) continue;
|
|
247
|
+
|
|
248
|
+
const configValue = getNestedValue(configData, configKey);
|
|
249
|
+
if (configValue !== undefined) {
|
|
250
|
+
result[optionName] = configValue;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface ParseOptionsContext {
|
|
258
|
+
aliases?: Record<string, string>;
|
|
259
|
+
envBindings?: Record<string, string[]>;
|
|
260
|
+
configKeys?: Record<string, string>;
|
|
261
|
+
configData?: Record<string, unknown>;
|
|
262
|
+
env?: Record<string, string | undefined>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Combined preprocessing of options with all features.
|
|
267
|
+
* Precedence order (highest to lowest): CLI args > env vars > config file
|
|
268
|
+
*/
|
|
269
|
+
export function preprocessOptions(data: Record<string, unknown>, ctx: ParseOptionsContext): Record<string, unknown> {
|
|
270
|
+
let result = { ...data };
|
|
271
|
+
|
|
272
|
+
// 1. Apply aliases first
|
|
273
|
+
if (ctx.aliases && Object.keys(ctx.aliases).length > 0) {
|
|
274
|
+
result = preprocessAliases(result, ctx.aliases);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 2. Apply environment variables (higher precedence than config)
|
|
278
|
+
// These only apply if CLI didn't set the option
|
|
279
|
+
if (ctx.envBindings && Object.keys(ctx.envBindings).length > 0) {
|
|
280
|
+
result = applyEnvBindings(result, ctx.envBindings, ctx.env);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 3. Apply config file values (lowest precedence)
|
|
284
|
+
// These only apply if neither CLI nor env set the option
|
|
285
|
+
if (ctx.configKeys && ctx.configData) {
|
|
286
|
+
result = applyConfigValues(result, ctx.configKeys, ctx.configData);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
}
|