@fuzdev/fuz_util 0.47.0 → 0.48.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.
- package/dist/args.d.ts +138 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +447 -0
- package/dist/source_json.d.ts +8 -8
- package/package.json +2 -2
- package/src/lib/args.ts +546 -0
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* CLI arguments container.
|
|
4
|
+
* Positional arguments stored in `_`, named flags/options as string keys.
|
|
5
|
+
* Produced by `argv_parse` or external parsers (mri, minimist, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export interface Args {
|
|
8
|
+
_?: Array<string>;
|
|
9
|
+
[key: string]: ArgValue;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parsed CLI arguments with guaranteed positionals array.
|
|
13
|
+
* Returned by `argv_parse` which always initializes `_`.
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedArgs extends Args {
|
|
16
|
+
_: Array<string>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Value types supported in CLI arguments.
|
|
20
|
+
*/
|
|
21
|
+
export type ArgValue = string | number | boolean | undefined | Array<string | number | boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Schema description for help text generation.
|
|
24
|
+
* Not used by args_parse/args_serialize directly - provided for consumers
|
|
25
|
+
* building CLI help output.
|
|
26
|
+
*/
|
|
27
|
+
export interface ArgSchema {
|
|
28
|
+
type: string;
|
|
29
|
+
default: ArgValue;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result of alias extraction from a schema.
|
|
34
|
+
* Includes canonical keys for downstream conflict detection.
|
|
35
|
+
*/
|
|
36
|
+
export interface ArgsAliasesResult {
|
|
37
|
+
aliases: Map<string, string>;
|
|
38
|
+
canonical_keys: Set<string>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validates a zod schema for CLI arg usage.
|
|
42
|
+
*
|
|
43
|
+
* Checks for:
|
|
44
|
+
* - Alias conflicts with canonical keys
|
|
45
|
+
* - Duplicate aliases across different keys
|
|
46
|
+
*
|
|
47
|
+
* Results are cached per schema (WeakMap). Safe to call multiple times.
|
|
48
|
+
*
|
|
49
|
+
* @param schema Zod object schema with optional alias metadata
|
|
50
|
+
* @returns Validation result with success flag and optional error
|
|
51
|
+
*/
|
|
52
|
+
export declare const args_validate_schema: (schema: z.ZodType) => {
|
|
53
|
+
success: true;
|
|
54
|
+
} | {
|
|
55
|
+
success: false;
|
|
56
|
+
error: z.ZodError;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Validates parsed CLI args against a zod schema.
|
|
60
|
+
*
|
|
61
|
+
* Handles CLI-specific concerns before validation:
|
|
62
|
+
* 1. Validates schema (cached) - returns error if alias conflicts exist
|
|
63
|
+
* 2. Expands aliases defined in schema `.meta({aliases: ['v']})`
|
|
64
|
+
* 3. Strips alias keys (required for strictObject schemas)
|
|
65
|
+
* 4. Validates with zod
|
|
66
|
+
* 5. After validation, syncs `no-` prefixed boolean flags with their base keys
|
|
67
|
+
*
|
|
68
|
+
* Schema analysis is cached per schema (WeakMap) for performance.
|
|
69
|
+
*
|
|
70
|
+
* @param unparsed_args Args object from CLI parser (mri, minimist, etc.)
|
|
71
|
+
* @param schema Zod object schema with optional alias metadata
|
|
72
|
+
* @returns Zod SafeParseResult with expanded/synced data on success
|
|
73
|
+
*/
|
|
74
|
+
export declare const args_parse: <TOutput extends Record<string, ArgValue> = Args>(unparsed_args: Args, schema: z.ZodType<TOutput>) => z.ZodSafeParseResult<TOutput>;
|
|
75
|
+
/**
|
|
76
|
+
* Serializes Args to CLI string array for subprocess forwarding.
|
|
77
|
+
*
|
|
78
|
+
* Handles CLI conventions:
|
|
79
|
+
* - Positionals first, then flags
|
|
80
|
+
* - Single-char keys get single dash, multi-char get double dash
|
|
81
|
+
* - Boolean `true` becomes bare flag, `false` is skipped
|
|
82
|
+
* - `undefined` values are skipped
|
|
83
|
+
* - `no-` prefixed keys skipped when base key is truthy (avoid contradiction)
|
|
84
|
+
* - When schema provided, extracts aliases and prefers shortest form
|
|
85
|
+
*
|
|
86
|
+
* Schema analysis is cached per schema (WeakMap) for performance.
|
|
87
|
+
*
|
|
88
|
+
* @param args Args object to serialize
|
|
89
|
+
* @param schema Optional zod schema to extract aliases for short form preference
|
|
90
|
+
* @returns Array of CLI argument strings
|
|
91
|
+
*/
|
|
92
|
+
export declare const args_serialize: (args: Args, schema?: z.ZodType) => Array<string>;
|
|
93
|
+
/**
|
|
94
|
+
* Extracts alias mappings and canonical keys from a zod schema's metadata.
|
|
95
|
+
*
|
|
96
|
+
* Useful for consumers building custom tooling (help generators, conflict detection, etc.).
|
|
97
|
+
* Results are cached per schema (WeakMap).
|
|
98
|
+
*
|
|
99
|
+
* Note: Returns copies of the cached data to prevent mutation of internal cache.
|
|
100
|
+
*
|
|
101
|
+
* @param schema Zod object schema with optional `.meta({aliases})` on fields
|
|
102
|
+
* @returns Object with aliases map and canonical_keys set
|
|
103
|
+
*/
|
|
104
|
+
export declare const args_extract_aliases: (schema: z.ZodType) => ArgsAliasesResult;
|
|
105
|
+
/**
|
|
106
|
+
* Parses raw CLI argv array into an Args object.
|
|
107
|
+
*
|
|
108
|
+
* A lightweight, dependency-free alternative to mri/minimist with compatible behavior.
|
|
109
|
+
*
|
|
110
|
+
* Features:
|
|
111
|
+
* - `--flag` → `{flag: true}`
|
|
112
|
+
* - `--flag value` → `{flag: 'value'}` or `{flag: 123}` (numeric coercion)
|
|
113
|
+
* - `--flag=value` → equals syntax
|
|
114
|
+
* - `--flag=` → `{flag: ''}` (empty string, differs from mri which returns true)
|
|
115
|
+
* - `-f` → `{f: true}` (short flag)
|
|
116
|
+
* - `-f value` → `{f: 'value'}`
|
|
117
|
+
* - `-abc` → `{a: true, b: true, c: true}` (combined short flags)
|
|
118
|
+
* - `-abc value` → `{a: true, b: true, c: 'value'}` (last flag gets value)
|
|
119
|
+
* - `--no-flag` → `{flag: false}` (negation prefix)
|
|
120
|
+
* - `--` → stops flag parsing, rest become positionals
|
|
121
|
+
* - Positionals collected in `_` array
|
|
122
|
+
* - Repeated flags become arrays
|
|
123
|
+
*
|
|
124
|
+
* Intentional differences from mri:
|
|
125
|
+
* - `--flag=` returns `''` (mri returns `true`)
|
|
126
|
+
* - `--flag= next` returns `{flag: '', _: ['next']}` (mri takes `next` as the value)
|
|
127
|
+
* - `---flag` returns `{'-flag': true}` (mri strips all dashes)
|
|
128
|
+
* - `['--flag', '']` preserves `''` (mri coerces to `0`)
|
|
129
|
+
* - `--__proto__` works as a normal key (mri silently fails)
|
|
130
|
+
*
|
|
131
|
+
* The returned object uses `Object.create(null)` to prevent prototype pollution
|
|
132
|
+
* and allow any key name including `__proto__` and `constructor`.
|
|
133
|
+
*
|
|
134
|
+
* @param argv Raw argument array (typically process.argv.slice(2))
|
|
135
|
+
* @returns Parsed Args object with guaranteed `_` array (null prototype)
|
|
136
|
+
*/
|
|
137
|
+
export declare const argv_parse: (argv: Array<string>) => ParsedArgs;
|
|
138
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,IAAI;IACpB,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAW,SAAQ,IAAI;IACvC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEhG;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,QAAQ,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAsID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,QAAQ,CAAC,CAAC,OAAO,KACf;IAAC,OAAO,EAAE,IAAI,CAAA;CAAC,GAAG;IAAC,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAA;CAMtD,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,IAAI,EACzE,eAAe,IAAI,EACnB,QAAQ,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,KACxB,CAAC,CAAC,kBAAkB,CAAC,OAAO,CA6C9B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,IAAI,EAAE,SAAS,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,MAAM,CA2D3E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,iBAMxD,CAAC;AAgCF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,KAAK,CAAC,MAAM,CAAC,KAAG,UAyHhD,CAAC"}
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// WeakMap cache for schema analysis - avoids redundant reflection
|
|
3
|
+
const schema_cache = new WeakMap();
|
|
4
|
+
// Internal: Unwrap nested schema types (Optional, Default, Transform, Pipe)
|
|
5
|
+
const unwrap_schema = (def) => {
|
|
6
|
+
if ('innerType' in def)
|
|
7
|
+
return def.innerType; // Optional, Nullable
|
|
8
|
+
if ('in' in def)
|
|
9
|
+
return def.in; // Pipe
|
|
10
|
+
if ('schema' in def)
|
|
11
|
+
return def.schema; // Default, Transform
|
|
12
|
+
return undefined;
|
|
13
|
+
};
|
|
14
|
+
// Internal: Check if schema type is boolean (recursing through wrappers)
|
|
15
|
+
const is_boolean_field = (schema) => {
|
|
16
|
+
const def = schema._zod.def;
|
|
17
|
+
if (def.type === 'boolean')
|
|
18
|
+
return true;
|
|
19
|
+
const inner = unwrap_schema(def);
|
|
20
|
+
if (inner)
|
|
21
|
+
return is_boolean_field(inner);
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
// Internal: Analyze schema for aliases, canonical keys, boolean keys, and conflicts
|
|
25
|
+
const analyze_schema = (schema) => {
|
|
26
|
+
const aliases = new Map();
|
|
27
|
+
const canonical_keys = new Set();
|
|
28
|
+
const boolean_keys = new Set();
|
|
29
|
+
const errors = [];
|
|
30
|
+
const def = schema._zod.def;
|
|
31
|
+
// Unwrap to get object def (handle wrapped types like optional, default, etc.)
|
|
32
|
+
let obj_def = def;
|
|
33
|
+
while (!('shape' in obj_def)) {
|
|
34
|
+
const inner = unwrap_schema(obj_def);
|
|
35
|
+
if (!inner)
|
|
36
|
+
return { aliases, canonical_keys, boolean_keys, errors };
|
|
37
|
+
obj_def = inner._zod.def;
|
|
38
|
+
}
|
|
39
|
+
const shape = obj_def.shape;
|
|
40
|
+
// First pass: collect all canonical keys
|
|
41
|
+
for (const key of Object.keys(shape)) {
|
|
42
|
+
canonical_keys.add(key);
|
|
43
|
+
}
|
|
44
|
+
// Second pass: process fields for aliases and booleans
|
|
45
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
46
|
+
const field_schema = field;
|
|
47
|
+
// Track boolean fields for no- prefix sync
|
|
48
|
+
if (is_boolean_field(field_schema)) {
|
|
49
|
+
boolean_keys.add(key);
|
|
50
|
+
}
|
|
51
|
+
const meta = field_schema.meta();
|
|
52
|
+
if (meta?.aliases) {
|
|
53
|
+
for (const alias of meta.aliases) {
|
|
54
|
+
// Check for alias-canonical conflict
|
|
55
|
+
if (canonical_keys.has(alias)) {
|
|
56
|
+
errors.push({
|
|
57
|
+
type: 'alias_canonical_conflict',
|
|
58
|
+
alias,
|
|
59
|
+
canonical: key,
|
|
60
|
+
conflict_with: alias,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Check for duplicate alias
|
|
64
|
+
else if (aliases.has(alias)) {
|
|
65
|
+
errors.push({
|
|
66
|
+
type: 'duplicate_alias',
|
|
67
|
+
alias,
|
|
68
|
+
canonical: key,
|
|
69
|
+
conflict_with: aliases.get(alias),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
aliases.set(alias, key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { aliases, canonical_keys, boolean_keys, errors };
|
|
79
|
+
};
|
|
80
|
+
// Internal: Convert analysis errors to ZodError for consistent API
|
|
81
|
+
const to_conflict_error = (errors) => {
|
|
82
|
+
return new z.ZodError(errors.map((err) => ({
|
|
83
|
+
code: 'custom',
|
|
84
|
+
path: [err.alias],
|
|
85
|
+
message: err.type === 'alias_canonical_conflict'
|
|
86
|
+
? `Alias '${err.alias}' for '${err.canonical}' conflicts with canonical key '${err.conflict_with}'`
|
|
87
|
+
: `Alias '${err.alias}' is used by both '${err.canonical}' and '${err.conflict_with}'`,
|
|
88
|
+
})));
|
|
89
|
+
};
|
|
90
|
+
// Internal: Get or create cache entry for schema
|
|
91
|
+
const get_schema_cache = (schema) => {
|
|
92
|
+
let entry = schema_cache.get(schema);
|
|
93
|
+
if (!entry) {
|
|
94
|
+
const analysis = analyze_schema(schema);
|
|
95
|
+
entry = {
|
|
96
|
+
aliases: analysis.aliases,
|
|
97
|
+
canonical_keys: analysis.canonical_keys,
|
|
98
|
+
boolean_keys: analysis.boolean_keys,
|
|
99
|
+
conflict_error: analysis.errors.length > 0 ? to_conflict_error(analysis.errors) : null,
|
|
100
|
+
};
|
|
101
|
+
schema_cache.set(schema, entry);
|
|
102
|
+
}
|
|
103
|
+
return entry;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Validates a zod schema for CLI arg usage.
|
|
107
|
+
*
|
|
108
|
+
* Checks for:
|
|
109
|
+
* - Alias conflicts with canonical keys
|
|
110
|
+
* - Duplicate aliases across different keys
|
|
111
|
+
*
|
|
112
|
+
* Results are cached per schema (WeakMap). Safe to call multiple times.
|
|
113
|
+
*
|
|
114
|
+
* @param schema Zod object schema with optional alias metadata
|
|
115
|
+
* @returns Validation result with success flag and optional error
|
|
116
|
+
*/
|
|
117
|
+
export const args_validate_schema = (schema) => {
|
|
118
|
+
const cache = get_schema_cache(schema);
|
|
119
|
+
if (cache.conflict_error) {
|
|
120
|
+
return { success: false, error: cache.conflict_error };
|
|
121
|
+
}
|
|
122
|
+
return { success: true };
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Validates parsed CLI args against a zod schema.
|
|
126
|
+
*
|
|
127
|
+
* Handles CLI-specific concerns before validation:
|
|
128
|
+
* 1. Validates schema (cached) - returns error if alias conflicts exist
|
|
129
|
+
* 2. Expands aliases defined in schema `.meta({aliases: ['v']})`
|
|
130
|
+
* 3. Strips alias keys (required for strictObject schemas)
|
|
131
|
+
* 4. Validates with zod
|
|
132
|
+
* 5. After validation, syncs `no-` prefixed boolean flags with their base keys
|
|
133
|
+
*
|
|
134
|
+
* Schema analysis is cached per schema (WeakMap) for performance.
|
|
135
|
+
*
|
|
136
|
+
* @param unparsed_args Args object from CLI parser (mri, minimist, etc.)
|
|
137
|
+
* @param schema Zod object schema with optional alias metadata
|
|
138
|
+
* @returns Zod SafeParseResult with expanded/synced data on success
|
|
139
|
+
*/
|
|
140
|
+
export const args_parse = (unparsed_args, schema) => {
|
|
141
|
+
const cache = get_schema_cache(schema);
|
|
142
|
+
// Return conflict error if schema has issues
|
|
143
|
+
if (cache.conflict_error) {
|
|
144
|
+
return { success: false, error: cache.conflict_error };
|
|
145
|
+
}
|
|
146
|
+
// Build expanded args - copy canonical, expand aliases, strip alias keys
|
|
147
|
+
const expanded = {};
|
|
148
|
+
for (const [key, value] of Object.entries(unparsed_args)) {
|
|
149
|
+
if (cache.aliases.has(key)) {
|
|
150
|
+
const canonical = cache.aliases.get(key);
|
|
151
|
+
// Only expand if canonical not already present (canonical takes precedence)
|
|
152
|
+
if (!(canonical in expanded) && !(canonical in unparsed_args)) {
|
|
153
|
+
expanded[canonical] = value;
|
|
154
|
+
}
|
|
155
|
+
// Don't copy alias key (strip it)
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
expanded[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Validate with zod
|
|
162
|
+
const parsed = schema.safeParse(expanded);
|
|
163
|
+
if (parsed.success) {
|
|
164
|
+
// Mutate data with the correct source of truth for no- prefixed args
|
|
165
|
+
const data = parsed.data;
|
|
166
|
+
for (const key in data) {
|
|
167
|
+
if (key.startsWith('no-')) {
|
|
168
|
+
const base_key = key.substring(3);
|
|
169
|
+
// Only sync if both keys are booleans in the schema
|
|
170
|
+
if (cache.boolean_keys.has(key) && cache.boolean_keys.has(base_key)) {
|
|
171
|
+
if (!(key in unparsed_args) && !(key in expanded)) {
|
|
172
|
+
data[key] = !data[base_key];
|
|
173
|
+
}
|
|
174
|
+
else if (!(base_key in unparsed_args) && !(base_key in expanded)) {
|
|
175
|
+
data[base_key] = !data[key];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return parsed;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Serializes Args to CLI string array for subprocess forwarding.
|
|
185
|
+
*
|
|
186
|
+
* Handles CLI conventions:
|
|
187
|
+
* - Positionals first, then flags
|
|
188
|
+
* - Single-char keys get single dash, multi-char get double dash
|
|
189
|
+
* - Boolean `true` becomes bare flag, `false` is skipped
|
|
190
|
+
* - `undefined` values are skipped
|
|
191
|
+
* - `no-` prefixed keys skipped when base key is truthy (avoid contradiction)
|
|
192
|
+
* - When schema provided, extracts aliases and prefers shortest form
|
|
193
|
+
*
|
|
194
|
+
* Schema analysis is cached per schema (WeakMap) for performance.
|
|
195
|
+
*
|
|
196
|
+
* @param args Args object to serialize
|
|
197
|
+
* @param schema Optional zod schema to extract aliases for short form preference
|
|
198
|
+
* @returns Array of CLI argument strings
|
|
199
|
+
*/
|
|
200
|
+
export const args_serialize = (args, schema) => {
|
|
201
|
+
const result = [];
|
|
202
|
+
// Build reverse map (canonical → shortest alias) if schema provided
|
|
203
|
+
let shortest_names;
|
|
204
|
+
if (schema) {
|
|
205
|
+
const cache = get_schema_cache(schema);
|
|
206
|
+
shortest_names = new Map();
|
|
207
|
+
// Group aliases by canonical key
|
|
208
|
+
const aliases_by_canonical = new Map();
|
|
209
|
+
for (const [alias, canonical] of cache.aliases) {
|
|
210
|
+
if (!aliases_by_canonical.has(canonical)) {
|
|
211
|
+
aliases_by_canonical.set(canonical, []);
|
|
212
|
+
}
|
|
213
|
+
aliases_by_canonical.get(canonical).push(alias);
|
|
214
|
+
}
|
|
215
|
+
// Find shortest for each canonical
|
|
216
|
+
for (const [canonical, aliases] of aliases_by_canonical) {
|
|
217
|
+
const all_names = [canonical, ...aliases];
|
|
218
|
+
const shortest = all_names.reduce((a, b) => (a.length <= b.length ? a : b));
|
|
219
|
+
shortest_names.set(canonical, shortest);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const add_value = (name, value) => {
|
|
223
|
+
if (value === undefined)
|
|
224
|
+
return;
|
|
225
|
+
if (value === false)
|
|
226
|
+
return; // Can't represent false as bare flag
|
|
227
|
+
result.push(name);
|
|
228
|
+
if (typeof value !== 'boolean') {
|
|
229
|
+
result.push(value + '');
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
let positionals = null;
|
|
233
|
+
for (const [key, value] of Object.entries(args)) {
|
|
234
|
+
if (key === '_') {
|
|
235
|
+
positionals = value ? value.map((v) => v + '') : [];
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Skip no-X if X exists and is truthy
|
|
239
|
+
if (key.startsWith('no-')) {
|
|
240
|
+
const base = key.substring(3);
|
|
241
|
+
if (base in args && args[base]) {
|
|
242
|
+
continue; // Skip redundant no- flag
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Determine the name to use (prefer shortest if schema provided)
|
|
246
|
+
const use_key = shortest_names?.get(key) ?? key;
|
|
247
|
+
const name = `${use_key.length === 1 ? '-' : '--'}${use_key}`;
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
for (const v of value)
|
|
250
|
+
add_value(name, v);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
add_value(name, value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return positionals ? [...positionals, ...result] : result;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Extracts alias mappings and canonical keys from a zod schema's metadata.
|
|
261
|
+
*
|
|
262
|
+
* Useful for consumers building custom tooling (help generators, conflict detection, etc.).
|
|
263
|
+
* Results are cached per schema (WeakMap).
|
|
264
|
+
*
|
|
265
|
+
* Note: Returns copies of the cached data to prevent mutation of internal cache.
|
|
266
|
+
*
|
|
267
|
+
* @param schema Zod object schema with optional `.meta({aliases})` on fields
|
|
268
|
+
* @returns Object with aliases map and canonical_keys set
|
|
269
|
+
*/
|
|
270
|
+
export const args_extract_aliases = (schema) => {
|
|
271
|
+
const cache = get_schema_cache(schema);
|
|
272
|
+
return {
|
|
273
|
+
aliases: new Map(cache.aliases),
|
|
274
|
+
canonical_keys: new Set(cache.canonical_keys),
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
// Internal: Try to coerce a string value to number if it looks numeric
|
|
278
|
+
const coerce_value = (value) => {
|
|
279
|
+
// Handle empty string
|
|
280
|
+
if (value === '')
|
|
281
|
+
return value;
|
|
282
|
+
// Try to parse as number
|
|
283
|
+
const num = Number(value);
|
|
284
|
+
// Return number if valid and finite, otherwise keep as string
|
|
285
|
+
// This matches mri behavior: "123" -> 123, "12.5" -> 12.5, "1e5" -> 100000
|
|
286
|
+
// But "123abc" -> "123abc", "NaN" -> "NaN", "Infinity" -> "Infinity"
|
|
287
|
+
if (!Number.isNaN(num) && Number.isFinite(num) && value.trim() !== '') {
|
|
288
|
+
return num;
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
};
|
|
292
|
+
// Internal: Set a value on args, handling arrays for repeated flags
|
|
293
|
+
const set_arg = (args, key, value) => {
|
|
294
|
+
if (key in args) {
|
|
295
|
+
// Convert to array or push to existing array
|
|
296
|
+
const existing = args[key];
|
|
297
|
+
if (Array.isArray(existing)) {
|
|
298
|
+
existing.push(value);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
args[key] = [existing, value];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
args[key] = value;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Parses raw CLI argv array into an Args object.
|
|
310
|
+
*
|
|
311
|
+
* A lightweight, dependency-free alternative to mri/minimist with compatible behavior.
|
|
312
|
+
*
|
|
313
|
+
* Features:
|
|
314
|
+
* - `--flag` → `{flag: true}`
|
|
315
|
+
* - `--flag value` → `{flag: 'value'}` or `{flag: 123}` (numeric coercion)
|
|
316
|
+
* - `--flag=value` → equals syntax
|
|
317
|
+
* - `--flag=` → `{flag: ''}` (empty string, differs from mri which returns true)
|
|
318
|
+
* - `-f` → `{f: true}` (short flag)
|
|
319
|
+
* - `-f value` → `{f: 'value'}`
|
|
320
|
+
* - `-abc` → `{a: true, b: true, c: true}` (combined short flags)
|
|
321
|
+
* - `-abc value` → `{a: true, b: true, c: 'value'}` (last flag gets value)
|
|
322
|
+
* - `--no-flag` → `{flag: false}` (negation prefix)
|
|
323
|
+
* - `--` → stops flag parsing, rest become positionals
|
|
324
|
+
* - Positionals collected in `_` array
|
|
325
|
+
* - Repeated flags become arrays
|
|
326
|
+
*
|
|
327
|
+
* Intentional differences from mri:
|
|
328
|
+
* - `--flag=` returns `''` (mri returns `true`)
|
|
329
|
+
* - `--flag= next` returns `{flag: '', _: ['next']}` (mri takes `next` as the value)
|
|
330
|
+
* - `---flag` returns `{'-flag': true}` (mri strips all dashes)
|
|
331
|
+
* - `['--flag', '']` preserves `''` (mri coerces to `0`)
|
|
332
|
+
* - `--__proto__` works as a normal key (mri silently fails)
|
|
333
|
+
*
|
|
334
|
+
* The returned object uses `Object.create(null)` to prevent prototype pollution
|
|
335
|
+
* and allow any key name including `__proto__` and `constructor`.
|
|
336
|
+
*
|
|
337
|
+
* @param argv Raw argument array (typically process.argv.slice(2))
|
|
338
|
+
* @returns Parsed Args object with guaranteed `_` array (null prototype)
|
|
339
|
+
*/
|
|
340
|
+
export const argv_parse = (argv) => {
|
|
341
|
+
// Use Object.create(null) to allow __proto__ as a normal key
|
|
342
|
+
// This prevents prototype pollution and makes all key names work
|
|
343
|
+
const args = Object.create(null);
|
|
344
|
+
args._ = [];
|
|
345
|
+
const positionals = args._;
|
|
346
|
+
let i = 0;
|
|
347
|
+
let flags_done = false; // Set to true after seeing --
|
|
348
|
+
while (i < argv.length) {
|
|
349
|
+
const arg = argv[i];
|
|
350
|
+
// After --, everything is a positional
|
|
351
|
+
if (flags_done) {
|
|
352
|
+
positionals.push(arg);
|
|
353
|
+
i++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// -- stops flag parsing
|
|
357
|
+
if (arg === '--') {
|
|
358
|
+
flags_done = true;
|
|
359
|
+
i++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
// Long flag: --flag or --flag=value or --no-flag
|
|
363
|
+
if (arg.startsWith('--')) {
|
|
364
|
+
const rest = arg.slice(2);
|
|
365
|
+
// Handle --flag=value
|
|
366
|
+
const equals_index = rest.indexOf('=');
|
|
367
|
+
if (equals_index !== -1) {
|
|
368
|
+
const key = rest.slice(0, equals_index);
|
|
369
|
+
const value = rest.slice(equals_index + 1);
|
|
370
|
+
// Empty value after = becomes empty string (explicit value assignment)
|
|
371
|
+
// This differs from mri which treats it as boolean true
|
|
372
|
+
set_arg(args, key, coerce_value(value));
|
|
373
|
+
i++;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
// Handle --no-flag (negation) - includes --no- which sets '' to false
|
|
377
|
+
if (rest.startsWith('no-')) {
|
|
378
|
+
const key = rest.slice(3); // May be empty string for --no-
|
|
379
|
+
args[key] = false;
|
|
380
|
+
i++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// Handle --flag or --flag value
|
|
384
|
+
const key = rest;
|
|
385
|
+
const next = argv[i + 1];
|
|
386
|
+
// If next arg exists and doesn't look like a flag, it's the value
|
|
387
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
388
|
+
set_arg(args, key, coerce_value(next));
|
|
389
|
+
i += 2;
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Boolean flag
|
|
393
|
+
args[key] = true;
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// Single dash is ignored (matches mri)
|
|
399
|
+
if (arg === '-') {
|
|
400
|
+
i++;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// Short flag(s): -f or -abc or -f value
|
|
404
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
405
|
+
const chars = arg.slice(1);
|
|
406
|
+
// Handle -f=value (short flag with equals)
|
|
407
|
+
const equals_index = chars.indexOf('=');
|
|
408
|
+
if (equals_index !== -1) {
|
|
409
|
+
const key = chars.slice(0, equals_index);
|
|
410
|
+
const value = chars.slice(equals_index + 1);
|
|
411
|
+
// For -abc=value, set a and b to true, c gets value
|
|
412
|
+
for (let j = 0; j < key.length - 1; j++) {
|
|
413
|
+
args[key[j]] = true;
|
|
414
|
+
}
|
|
415
|
+
if (key.length > 0) {
|
|
416
|
+
set_arg(args, key[key.length - 1], coerce_value(value));
|
|
417
|
+
}
|
|
418
|
+
i++;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
// Handle combined flags: -abc means -a -b -c
|
|
422
|
+
// Last flag can take a value if next arg isn't a flag
|
|
423
|
+
const next = argv[i + 1];
|
|
424
|
+
const has_value = next !== undefined && !next.startsWith('-');
|
|
425
|
+
for (let j = 0; j < chars.length; j++) {
|
|
426
|
+
const char = chars[j];
|
|
427
|
+
const is_last = j === chars.length - 1;
|
|
428
|
+
if (is_last && has_value) {
|
|
429
|
+
// Last char gets the value
|
|
430
|
+
set_arg(args, char, coerce_value(next));
|
|
431
|
+
i += 2;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// Boolean flag
|
|
435
|
+
args[char] = true;
|
|
436
|
+
if (is_last)
|
|
437
|
+
i++;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// Positional argument
|
|
443
|
+
positionals.push(arg);
|
|
444
|
+
i++;
|
|
445
|
+
}
|
|
446
|
+
return args;
|
|
447
|
+
};
|
package/dist/source_json.d.ts
CHANGED
|
@@ -15,11 +15,11 @@ import { z } from 'zod';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const DeclarationKind: z.ZodEnum<{
|
|
17
17
|
function: "function";
|
|
18
|
-
json: "json";
|
|
19
18
|
type: "type";
|
|
19
|
+
constructor: "constructor";
|
|
20
|
+
json: "json";
|
|
20
21
|
variable: "variable";
|
|
21
22
|
class: "class";
|
|
22
|
-
constructor: "constructor";
|
|
23
23
|
component: "component";
|
|
24
24
|
css: "css";
|
|
25
25
|
}>;
|
|
@@ -79,11 +79,11 @@ export declare const DeclarationJson: z.ZodObject<{
|
|
|
79
79
|
name: z.ZodString;
|
|
80
80
|
kind: z.ZodEnum<{
|
|
81
81
|
function: "function";
|
|
82
|
-
json: "json";
|
|
83
82
|
type: "type";
|
|
83
|
+
constructor: "constructor";
|
|
84
|
+
json: "json";
|
|
84
85
|
variable: "variable";
|
|
85
86
|
class: "class";
|
|
86
|
-
constructor: "constructor";
|
|
87
87
|
component: "component";
|
|
88
88
|
css: "css";
|
|
89
89
|
}>;
|
|
@@ -171,11 +171,11 @@ export declare const ModuleJson: z.ZodObject<{
|
|
|
171
171
|
name: z.ZodString;
|
|
172
172
|
kind: z.ZodEnum<{
|
|
173
173
|
function: "function";
|
|
174
|
-
json: "json";
|
|
175
174
|
type: "type";
|
|
175
|
+
constructor: "constructor";
|
|
176
|
+
json: "json";
|
|
176
177
|
variable: "variable";
|
|
177
178
|
class: "class";
|
|
178
|
-
constructor: "constructor";
|
|
179
179
|
component: "component";
|
|
180
180
|
css: "css";
|
|
181
181
|
}>;
|
|
@@ -278,11 +278,11 @@ export declare const SourceJson: z.ZodObject<{
|
|
|
278
278
|
name: z.ZodString;
|
|
279
279
|
kind: z.ZodEnum<{
|
|
280
280
|
function: "function";
|
|
281
|
-
json: "json";
|
|
282
281
|
type: "type";
|
|
282
|
+
constructor: "constructor";
|
|
283
|
+
json: "json";
|
|
283
284
|
variable: "variable";
|
|
284
285
|
class: "class";
|
|
285
|
-
constructor: "constructor";
|
|
286
286
|
component: "component";
|
|
287
287
|
css: "css";
|
|
288
288
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "utility belt for JS",
|
|
5
5
|
"glyph": "🦕",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@fuzdev/fuz_css": "^0.44.1",
|
|
66
66
|
"@fuzdev/fuz_ui": "^0.179.0",
|
|
67
67
|
"@ryanatkn/eslint-config": "^0.9.0",
|
|
68
|
-
"@ryanatkn/gro": "^0.
|
|
68
|
+
"@ryanatkn/gro": "^0.188.0",
|
|
69
69
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
70
70
|
"@sveltejs/kit": "^2.50.1",
|
|
71
71
|
"@sveltejs/package": "^2.5.7",
|
package/src/lib/args.ts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI arguments container.
|
|
5
|
+
* Positional arguments stored in `_`, named flags/options as string keys.
|
|
6
|
+
* Produced by `argv_parse` or external parsers (mri, minimist, etc.).
|
|
7
|
+
*/
|
|
8
|
+
export interface Args {
|
|
9
|
+
_?: Array<string>;
|
|
10
|
+
[key: string]: ArgValue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parsed CLI arguments with guaranteed positionals array.
|
|
15
|
+
* Returned by `argv_parse` which always initializes `_`.
|
|
16
|
+
*/
|
|
17
|
+
export interface ParsedArgs extends Args {
|
|
18
|
+
_: Array<string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Value types supported in CLI arguments.
|
|
23
|
+
*/
|
|
24
|
+
export type ArgValue = string | number | boolean | undefined | Array<string | number | boolean>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Schema description for help text generation.
|
|
28
|
+
* Not used by args_parse/args_serialize directly - provided for consumers
|
|
29
|
+
* building CLI help output.
|
|
30
|
+
*/
|
|
31
|
+
export interface ArgSchema {
|
|
32
|
+
type: string;
|
|
33
|
+
default: ArgValue;
|
|
34
|
+
description: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Result of alias extraction from a schema.
|
|
39
|
+
* Includes canonical keys for downstream conflict detection.
|
|
40
|
+
*/
|
|
41
|
+
export interface ArgsAliasesResult {
|
|
42
|
+
aliases: Map<string, string>; // alias → canonical
|
|
43
|
+
canonical_keys: Set<string>; // all canonical key names
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Internal cache entry structure
|
|
47
|
+
interface SchemaCacheEntry {
|
|
48
|
+
aliases: Map<string, string>; // alias → canonical
|
|
49
|
+
canonical_keys: Set<string>; // all canonical key names
|
|
50
|
+
boolean_keys: Set<string>; // keys with boolean type (for no- sync)
|
|
51
|
+
conflict_error: z.ZodError | null; // null if schema is valid
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// WeakMap cache for schema analysis - avoids redundant reflection
|
|
55
|
+
const schema_cache: WeakMap<z.ZodType, SchemaCacheEntry> = new WeakMap();
|
|
56
|
+
|
|
57
|
+
// Internal: Unwrap nested schema types (Optional, Default, Transform, Pipe)
|
|
58
|
+
const unwrap_schema = (def: z.core.$ZodTypeDef): z.ZodType | undefined => {
|
|
59
|
+
if ('innerType' in def) return def.innerType as z.ZodType; // Optional, Nullable
|
|
60
|
+
if ('in' in def) return def.in as z.ZodType; // Pipe
|
|
61
|
+
if ('schema' in def) return def.schema as z.ZodType; // Default, Transform
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Internal: Check if schema type is boolean (recursing through wrappers)
|
|
66
|
+
const is_boolean_field = (schema: z.ZodType): boolean => {
|
|
67
|
+
const def = schema._zod.def;
|
|
68
|
+
if (def.type === 'boolean') return true;
|
|
69
|
+
const inner = unwrap_schema(def);
|
|
70
|
+
if (inner) return is_boolean_field(inner);
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Internal: Schema analysis result
|
|
75
|
+
interface SchemaAnalysisResult {
|
|
76
|
+
aliases: Map<string, string>;
|
|
77
|
+
canonical_keys: Set<string>;
|
|
78
|
+
boolean_keys: Set<string>;
|
|
79
|
+
errors: Array<{
|
|
80
|
+
type: 'alias_canonical_conflict' | 'duplicate_alias';
|
|
81
|
+
alias: string;
|
|
82
|
+
canonical: string;
|
|
83
|
+
conflict_with: string;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Internal: Analyze schema for aliases, canonical keys, boolean keys, and conflicts
|
|
88
|
+
const analyze_schema = (schema: z.ZodType): SchemaAnalysisResult => {
|
|
89
|
+
const aliases: Map<string, string> = new Map();
|
|
90
|
+
const canonical_keys: Set<string> = new Set();
|
|
91
|
+
const boolean_keys: Set<string> = new Set();
|
|
92
|
+
const errors: SchemaAnalysisResult['errors'] = [];
|
|
93
|
+
const def = schema._zod.def;
|
|
94
|
+
|
|
95
|
+
// Unwrap to get object def (handle wrapped types like optional, default, etc.)
|
|
96
|
+
let obj_def = def;
|
|
97
|
+
while (!('shape' in obj_def)) {
|
|
98
|
+
const inner = unwrap_schema(obj_def);
|
|
99
|
+
if (!inner) return {aliases, canonical_keys, boolean_keys, errors};
|
|
100
|
+
obj_def = inner._zod.def;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const shape = (obj_def as z.core.$ZodObjectDef).shape;
|
|
104
|
+
|
|
105
|
+
// First pass: collect all canonical keys
|
|
106
|
+
for (const key of Object.keys(shape)) {
|
|
107
|
+
canonical_keys.add(key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Second pass: process fields for aliases and booleans
|
|
111
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
112
|
+
const field_schema = field as z.ZodType;
|
|
113
|
+
|
|
114
|
+
// Track boolean fields for no- prefix sync
|
|
115
|
+
if (is_boolean_field(field_schema)) {
|
|
116
|
+
boolean_keys.add(key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const meta = field_schema.meta();
|
|
120
|
+
if (meta?.aliases) {
|
|
121
|
+
for (const alias of meta.aliases as Array<string>) {
|
|
122
|
+
// Check for alias-canonical conflict
|
|
123
|
+
if (canonical_keys.has(alias)) {
|
|
124
|
+
errors.push({
|
|
125
|
+
type: 'alias_canonical_conflict',
|
|
126
|
+
alias,
|
|
127
|
+
canonical: key,
|
|
128
|
+
conflict_with: alias,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Check for duplicate alias
|
|
132
|
+
else if (aliases.has(alias)) {
|
|
133
|
+
errors.push({
|
|
134
|
+
type: 'duplicate_alias',
|
|
135
|
+
alias,
|
|
136
|
+
canonical: key,
|
|
137
|
+
conflict_with: aliases.get(alias)!,
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
aliases.set(alias, key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {aliases, canonical_keys, boolean_keys, errors};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Internal: Convert analysis errors to ZodError for consistent API
|
|
149
|
+
const to_conflict_error = (errors: SchemaAnalysisResult['errors']): z.ZodError => {
|
|
150
|
+
return new z.ZodError(
|
|
151
|
+
errors.map((err) => ({
|
|
152
|
+
code: 'custom' as const,
|
|
153
|
+
path: [err.alias],
|
|
154
|
+
message:
|
|
155
|
+
err.type === 'alias_canonical_conflict'
|
|
156
|
+
? `Alias '${err.alias}' for '${err.canonical}' conflicts with canonical key '${err.conflict_with}'`
|
|
157
|
+
: `Alias '${err.alias}' is used by both '${err.canonical}' and '${err.conflict_with}'`,
|
|
158
|
+
})),
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Internal: Get or create cache entry for schema
|
|
163
|
+
const get_schema_cache = (schema: z.ZodType): SchemaCacheEntry => {
|
|
164
|
+
let entry = schema_cache.get(schema);
|
|
165
|
+
if (!entry) {
|
|
166
|
+
const analysis = analyze_schema(schema);
|
|
167
|
+
entry = {
|
|
168
|
+
aliases: analysis.aliases,
|
|
169
|
+
canonical_keys: analysis.canonical_keys,
|
|
170
|
+
boolean_keys: analysis.boolean_keys,
|
|
171
|
+
conflict_error: analysis.errors.length > 0 ? to_conflict_error(analysis.errors) : null,
|
|
172
|
+
};
|
|
173
|
+
schema_cache.set(schema, entry);
|
|
174
|
+
}
|
|
175
|
+
return entry;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validates a zod schema for CLI arg usage.
|
|
180
|
+
*
|
|
181
|
+
* Checks for:
|
|
182
|
+
* - Alias conflicts with canonical keys
|
|
183
|
+
* - Duplicate aliases across different keys
|
|
184
|
+
*
|
|
185
|
+
* Results are cached per schema (WeakMap). Safe to call multiple times.
|
|
186
|
+
*
|
|
187
|
+
* @param schema Zod object schema with optional alias metadata
|
|
188
|
+
* @returns Validation result with success flag and optional error
|
|
189
|
+
*/
|
|
190
|
+
export const args_validate_schema = (
|
|
191
|
+
schema: z.ZodType,
|
|
192
|
+
): {success: true} | {success: false; error: z.ZodError} => {
|
|
193
|
+
const cache = get_schema_cache(schema);
|
|
194
|
+
if (cache.conflict_error) {
|
|
195
|
+
return {success: false, error: cache.conflict_error};
|
|
196
|
+
}
|
|
197
|
+
return {success: true};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validates parsed CLI args against a zod schema.
|
|
202
|
+
*
|
|
203
|
+
* Handles CLI-specific concerns before validation:
|
|
204
|
+
* 1. Validates schema (cached) - returns error if alias conflicts exist
|
|
205
|
+
* 2. Expands aliases defined in schema `.meta({aliases: ['v']})`
|
|
206
|
+
* 3. Strips alias keys (required for strictObject schemas)
|
|
207
|
+
* 4. Validates with zod
|
|
208
|
+
* 5. After validation, syncs `no-` prefixed boolean flags with their base keys
|
|
209
|
+
*
|
|
210
|
+
* Schema analysis is cached per schema (WeakMap) for performance.
|
|
211
|
+
*
|
|
212
|
+
* @param unparsed_args Args object from CLI parser (mri, minimist, etc.)
|
|
213
|
+
* @param schema Zod object schema with optional alias metadata
|
|
214
|
+
* @returns Zod SafeParseResult with expanded/synced data on success
|
|
215
|
+
*/
|
|
216
|
+
export const args_parse = <TOutput extends Record<string, ArgValue> = Args>(
|
|
217
|
+
unparsed_args: Args,
|
|
218
|
+
schema: z.ZodType<TOutput>,
|
|
219
|
+
): z.ZodSafeParseResult<TOutput> => {
|
|
220
|
+
const cache = get_schema_cache(schema);
|
|
221
|
+
|
|
222
|
+
// Return conflict error if schema has issues
|
|
223
|
+
if (cache.conflict_error) {
|
|
224
|
+
return {success: false, error: cache.conflict_error as z.ZodError<TOutput>};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build expanded args - copy canonical, expand aliases, strip alias keys
|
|
228
|
+
const expanded: Record<string, ArgValue> = {};
|
|
229
|
+
for (const [key, value] of Object.entries(unparsed_args)) {
|
|
230
|
+
if (cache.aliases.has(key)) {
|
|
231
|
+
const canonical = cache.aliases.get(key)!;
|
|
232
|
+
// Only expand if canonical not already present (canonical takes precedence)
|
|
233
|
+
if (!(canonical in expanded) && !(canonical in unparsed_args)) {
|
|
234
|
+
expanded[canonical] = value;
|
|
235
|
+
}
|
|
236
|
+
// Don't copy alias key (strip it)
|
|
237
|
+
} else {
|
|
238
|
+
expanded[key] = value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Validate with zod
|
|
243
|
+
const parsed = schema.safeParse(expanded);
|
|
244
|
+
|
|
245
|
+
if (parsed.success) {
|
|
246
|
+
// Mutate data with the correct source of truth for no- prefixed args
|
|
247
|
+
const data = parsed.data as Record<string, ArgValue>;
|
|
248
|
+
for (const key in data) {
|
|
249
|
+
if (key.startsWith('no-')) {
|
|
250
|
+
const base_key = key.substring(3);
|
|
251
|
+
// Only sync if both keys are booleans in the schema
|
|
252
|
+
if (cache.boolean_keys.has(key) && cache.boolean_keys.has(base_key)) {
|
|
253
|
+
if (!(key in unparsed_args) && !(key in expanded)) {
|
|
254
|
+
data[key] = !data[base_key];
|
|
255
|
+
} else if (!(base_key in unparsed_args) && !(base_key in expanded)) {
|
|
256
|
+
data[base_key] = !data[key];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return parsed;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Serializes Args to CLI string array for subprocess forwarding.
|
|
268
|
+
*
|
|
269
|
+
* Handles CLI conventions:
|
|
270
|
+
* - Positionals first, then flags
|
|
271
|
+
* - Single-char keys get single dash, multi-char get double dash
|
|
272
|
+
* - Boolean `true` becomes bare flag, `false` is skipped
|
|
273
|
+
* - `undefined` values are skipped
|
|
274
|
+
* - `no-` prefixed keys skipped when base key is truthy (avoid contradiction)
|
|
275
|
+
* - When schema provided, extracts aliases and prefers shortest form
|
|
276
|
+
*
|
|
277
|
+
* Schema analysis is cached per schema (WeakMap) for performance.
|
|
278
|
+
*
|
|
279
|
+
* @param args Args object to serialize
|
|
280
|
+
* @param schema Optional zod schema to extract aliases for short form preference
|
|
281
|
+
* @returns Array of CLI argument strings
|
|
282
|
+
*/
|
|
283
|
+
export const args_serialize = (args: Args, schema?: z.ZodType): Array<string> => {
|
|
284
|
+
const result: Array<string> = [];
|
|
285
|
+
|
|
286
|
+
// Build reverse map (canonical → shortest alias) if schema provided
|
|
287
|
+
let shortest_names: Map<string, string> | undefined;
|
|
288
|
+
if (schema) {
|
|
289
|
+
const cache = get_schema_cache(schema);
|
|
290
|
+
shortest_names = new Map();
|
|
291
|
+
// Group aliases by canonical key
|
|
292
|
+
const aliases_by_canonical: Map<string, Array<string>> = new Map();
|
|
293
|
+
for (const [alias, canonical] of cache.aliases) {
|
|
294
|
+
if (!aliases_by_canonical.has(canonical)) {
|
|
295
|
+
aliases_by_canonical.set(canonical, []);
|
|
296
|
+
}
|
|
297
|
+
aliases_by_canonical.get(canonical)!.push(alias);
|
|
298
|
+
}
|
|
299
|
+
// Find shortest for each canonical
|
|
300
|
+
for (const [canonical, aliases] of aliases_by_canonical) {
|
|
301
|
+
const all_names = [canonical, ...aliases];
|
|
302
|
+
const shortest = all_names.reduce((a, b) => (a.length <= b.length ? a : b));
|
|
303
|
+
shortest_names.set(canonical, shortest);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const add_value = (name: string, value: string | number | boolean | undefined): void => {
|
|
308
|
+
if (value === undefined) return;
|
|
309
|
+
if (value === false) return; // Can't represent false as bare flag
|
|
310
|
+
result.push(name);
|
|
311
|
+
if (typeof value !== 'boolean') {
|
|
312
|
+
result.push(value + '');
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
let positionals: Array<string> | null = null;
|
|
317
|
+
for (const [key, value] of Object.entries(args)) {
|
|
318
|
+
if (key === '_') {
|
|
319
|
+
positionals = value ? (value as Array<string | number | boolean>).map((v) => v + '') : [];
|
|
320
|
+
} else {
|
|
321
|
+
// Skip no-X if X exists and is truthy
|
|
322
|
+
if (key.startsWith('no-')) {
|
|
323
|
+
const base = key.substring(3);
|
|
324
|
+
if (base in args && args[base]) {
|
|
325
|
+
continue; // Skip redundant no- flag
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Determine the name to use (prefer shortest if schema provided)
|
|
330
|
+
const use_key = shortest_names?.get(key) ?? key;
|
|
331
|
+
const name = `${use_key.length === 1 ? '-' : '--'}${use_key}`;
|
|
332
|
+
|
|
333
|
+
if (Array.isArray(value)) {
|
|
334
|
+
for (const v of value) add_value(name, v);
|
|
335
|
+
} else {
|
|
336
|
+
add_value(name, value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return positionals ? [...positionals, ...result] : result;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Extracts alias mappings and canonical keys from a zod schema's metadata.
|
|
346
|
+
*
|
|
347
|
+
* Useful for consumers building custom tooling (help generators, conflict detection, etc.).
|
|
348
|
+
* Results are cached per schema (WeakMap).
|
|
349
|
+
*
|
|
350
|
+
* Note: Returns copies of the cached data to prevent mutation of internal cache.
|
|
351
|
+
*
|
|
352
|
+
* @param schema Zod object schema with optional `.meta({aliases})` on fields
|
|
353
|
+
* @returns Object with aliases map and canonical_keys set
|
|
354
|
+
*/
|
|
355
|
+
export const args_extract_aliases = (schema: z.ZodType): ArgsAliasesResult => {
|
|
356
|
+
const cache = get_schema_cache(schema);
|
|
357
|
+
return {
|
|
358
|
+
aliases: new Map(cache.aliases),
|
|
359
|
+
canonical_keys: new Set(cache.canonical_keys),
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Internal: Try to coerce a string value to number if it looks numeric
|
|
364
|
+
const coerce_value = (value: string): string | number => {
|
|
365
|
+
// Handle empty string
|
|
366
|
+
if (value === '') return value;
|
|
367
|
+
// Try to parse as number
|
|
368
|
+
const num = Number(value);
|
|
369
|
+
// Return number if valid and finite, otherwise keep as string
|
|
370
|
+
// This matches mri behavior: "123" -> 123, "12.5" -> 12.5, "1e5" -> 100000
|
|
371
|
+
// But "123abc" -> "123abc", "NaN" -> "NaN", "Infinity" -> "Infinity"
|
|
372
|
+
if (!Number.isNaN(num) && Number.isFinite(num) && value.trim() !== '') {
|
|
373
|
+
return num;
|
|
374
|
+
}
|
|
375
|
+
return value;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Internal: Set a value on args, handling arrays for repeated flags
|
|
379
|
+
const set_arg = (args: Args, key: string, value: string | number | boolean): void => {
|
|
380
|
+
if (key in args) {
|
|
381
|
+
// Convert to array or push to existing array
|
|
382
|
+
const existing = args[key];
|
|
383
|
+
if (Array.isArray(existing)) {
|
|
384
|
+
existing.push(value);
|
|
385
|
+
} else {
|
|
386
|
+
args[key] = [existing!, value];
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
args[key] = value;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Parses raw CLI argv array into an Args object.
|
|
395
|
+
*
|
|
396
|
+
* A lightweight, dependency-free alternative to mri/minimist with compatible behavior.
|
|
397
|
+
*
|
|
398
|
+
* Features:
|
|
399
|
+
* - `--flag` → `{flag: true}`
|
|
400
|
+
* - `--flag value` → `{flag: 'value'}` or `{flag: 123}` (numeric coercion)
|
|
401
|
+
* - `--flag=value` → equals syntax
|
|
402
|
+
* - `--flag=` → `{flag: ''}` (empty string, differs from mri which returns true)
|
|
403
|
+
* - `-f` → `{f: true}` (short flag)
|
|
404
|
+
* - `-f value` → `{f: 'value'}`
|
|
405
|
+
* - `-abc` → `{a: true, b: true, c: true}` (combined short flags)
|
|
406
|
+
* - `-abc value` → `{a: true, b: true, c: 'value'}` (last flag gets value)
|
|
407
|
+
* - `--no-flag` → `{flag: false}` (negation prefix)
|
|
408
|
+
* - `--` → stops flag parsing, rest become positionals
|
|
409
|
+
* - Positionals collected in `_` array
|
|
410
|
+
* - Repeated flags become arrays
|
|
411
|
+
*
|
|
412
|
+
* Intentional differences from mri:
|
|
413
|
+
* - `--flag=` returns `''` (mri returns `true`)
|
|
414
|
+
* - `--flag= next` returns `{flag: '', _: ['next']}` (mri takes `next` as the value)
|
|
415
|
+
* - `---flag` returns `{'-flag': true}` (mri strips all dashes)
|
|
416
|
+
* - `['--flag', '']` preserves `''` (mri coerces to `0`)
|
|
417
|
+
* - `--__proto__` works as a normal key (mri silently fails)
|
|
418
|
+
*
|
|
419
|
+
* The returned object uses `Object.create(null)` to prevent prototype pollution
|
|
420
|
+
* and allow any key name including `__proto__` and `constructor`.
|
|
421
|
+
*
|
|
422
|
+
* @param argv Raw argument array (typically process.argv.slice(2))
|
|
423
|
+
* @returns Parsed Args object with guaranteed `_` array (null prototype)
|
|
424
|
+
*/
|
|
425
|
+
export const argv_parse = (argv: Array<string>): ParsedArgs => {
|
|
426
|
+
// Use Object.create(null) to allow __proto__ as a normal key
|
|
427
|
+
// This prevents prototype pollution and makes all key names work
|
|
428
|
+
const args = Object.create(null) as ParsedArgs;
|
|
429
|
+
args._ = [];
|
|
430
|
+
const positionals = args._;
|
|
431
|
+
|
|
432
|
+
let i = 0;
|
|
433
|
+
let flags_done = false; // Set to true after seeing --
|
|
434
|
+
|
|
435
|
+
while (i < argv.length) {
|
|
436
|
+
const arg = argv[i]!;
|
|
437
|
+
|
|
438
|
+
// After --, everything is a positional
|
|
439
|
+
if (flags_done) {
|
|
440
|
+
positionals.push(arg);
|
|
441
|
+
i++;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// -- stops flag parsing
|
|
446
|
+
if (arg === '--') {
|
|
447
|
+
flags_done = true;
|
|
448
|
+
i++;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Long flag: --flag or --flag=value or --no-flag
|
|
453
|
+
if (arg.startsWith('--')) {
|
|
454
|
+
const rest = arg.slice(2);
|
|
455
|
+
|
|
456
|
+
// Handle --flag=value
|
|
457
|
+
const equals_index = rest.indexOf('=');
|
|
458
|
+
if (equals_index !== -1) {
|
|
459
|
+
const key = rest.slice(0, equals_index);
|
|
460
|
+
const value = rest.slice(equals_index + 1);
|
|
461
|
+
// Empty value after = becomes empty string (explicit value assignment)
|
|
462
|
+
// This differs from mri which treats it as boolean true
|
|
463
|
+
set_arg(args, key, coerce_value(value));
|
|
464
|
+
i++;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Handle --no-flag (negation) - includes --no- which sets '' to false
|
|
469
|
+
if (rest.startsWith('no-')) {
|
|
470
|
+
const key = rest.slice(3); // May be empty string for --no-
|
|
471
|
+
args[key] = false;
|
|
472
|
+
i++;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Handle --flag or --flag value
|
|
477
|
+
const key = rest;
|
|
478
|
+
const next = argv[i + 1];
|
|
479
|
+
|
|
480
|
+
// If next arg exists and doesn't look like a flag, it's the value
|
|
481
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
482
|
+
set_arg(args, key, coerce_value(next));
|
|
483
|
+
i += 2;
|
|
484
|
+
} else {
|
|
485
|
+
// Boolean flag
|
|
486
|
+
args[key] = true;
|
|
487
|
+
i++;
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Single dash is ignored (matches mri)
|
|
493
|
+
if (arg === '-') {
|
|
494
|
+
i++;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Short flag(s): -f or -abc or -f value
|
|
499
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
500
|
+
const chars = arg.slice(1);
|
|
501
|
+
|
|
502
|
+
// Handle -f=value (short flag with equals)
|
|
503
|
+
const equals_index = chars.indexOf('=');
|
|
504
|
+
if (equals_index !== -1) {
|
|
505
|
+
const key = chars.slice(0, equals_index);
|
|
506
|
+
const value = chars.slice(equals_index + 1);
|
|
507
|
+
// For -abc=value, set a and b to true, c gets value
|
|
508
|
+
for (let j = 0; j < key.length - 1; j++) {
|
|
509
|
+
args[key[j]!] = true;
|
|
510
|
+
}
|
|
511
|
+
if (key.length > 0) {
|
|
512
|
+
set_arg(args, key[key.length - 1]!, coerce_value(value));
|
|
513
|
+
}
|
|
514
|
+
i++;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Handle combined flags: -abc means -a -b -c
|
|
519
|
+
// Last flag can take a value if next arg isn't a flag
|
|
520
|
+
const next = argv[i + 1];
|
|
521
|
+
const has_value = next !== undefined && !next.startsWith('-');
|
|
522
|
+
|
|
523
|
+
for (let j = 0; j < chars.length; j++) {
|
|
524
|
+
const char = chars[j]!;
|
|
525
|
+
const is_last = j === chars.length - 1;
|
|
526
|
+
|
|
527
|
+
if (is_last && has_value) {
|
|
528
|
+
// Last char gets the value
|
|
529
|
+
set_arg(args, char, coerce_value(next));
|
|
530
|
+
i += 2;
|
|
531
|
+
} else {
|
|
532
|
+
// Boolean flag
|
|
533
|
+
args[char] = true;
|
|
534
|
+
if (is_last) i++;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Positional argument
|
|
541
|
+
positionals.push(arg);
|
|
542
|
+
i++;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return args;
|
|
546
|
+
};
|