@f5-sales-demo/pi-utils 20.2.6 → 20.3.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/package.json +2 -2
- package/src/cli.ts +117 -94
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-utils",
|
|
4
|
-
"version": "20.
|
|
4
|
+
"version": "20.3.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "^1.3",
|
|
42
|
-
"@f5-sales-demo/pi-natives": "20.
|
|
42
|
+
"@f5-sales-demo/pi-natives": "20.3.0"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"bun": ">=1.3.7"
|
package/src/cli.ts
CHANGED
|
@@ -116,6 +116,18 @@ export interface CommandCtor {
|
|
|
116
116
|
args?: Record<string, ArgDescriptor>;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export interface CommandSyntax {
|
|
120
|
+
strict?: boolean;
|
|
121
|
+
flags?: Record<string, FlagDescriptor>;
|
|
122
|
+
args?: Record<string, ArgDescriptor>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CommandParseResult {
|
|
126
|
+
flags: Record<string, unknown>;
|
|
127
|
+
args: Record<string, unknown>;
|
|
128
|
+
argv: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
/** Configuration passed to every command instance and help renderers. */
|
|
120
132
|
export interface CliConfig {
|
|
121
133
|
bin: string;
|
|
@@ -152,110 +164,115 @@ export abstract class Command {
|
|
|
152
164
|
: Record<string, ArgDescriptor>
|
|
153
165
|
>
|
|
154
166
|
> {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const strict = Cmd.strict !== false;
|
|
159
|
-
|
|
160
|
-
// Build node:util parseArgs options from flag descriptors
|
|
161
|
-
const options: Record<
|
|
162
|
-
string,
|
|
163
|
-
{ type: "string" | "boolean"; short?: string; multiple?: boolean; default?: string | boolean }
|
|
164
|
-
> = {};
|
|
165
|
-
for (const [name, desc] of Object.entries(flagDefs)) {
|
|
166
|
-
const opt: (typeof options)[string] = {
|
|
167
|
-
type: desc.kind === "boolean" ? "boolean" : "string",
|
|
168
|
-
};
|
|
169
|
-
if (desc.char) opt.short = desc.char;
|
|
170
|
-
if (desc.multiple) opt.multiple = true;
|
|
171
|
-
if (desc.default !== undefined) {
|
|
172
|
-
opt.default = desc.kind === "boolean" ? Boolean(desc.default) : String(desc.default);
|
|
173
|
-
}
|
|
174
|
-
options[name] = opt;
|
|
175
|
-
}
|
|
167
|
+
return parseCommandArgv(this.argv, _Cmd) as never;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
176
170
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (desc.kind === "integer") {
|
|
198
|
-
if (raw === undefined || typeof raw === "boolean") {
|
|
199
|
-
flags[name] = desc.default ?? undefined;
|
|
200
|
-
} else {
|
|
201
|
-
const n = Number.parseInt(raw as string, 10);
|
|
202
|
-
if (Number.isNaN(n)) {
|
|
203
|
-
throw new CliUsageError(`Expected integer for --${name}, got "${raw}"`);
|
|
204
|
-
}
|
|
205
|
-
flags[name] = n;
|
|
206
|
-
}
|
|
207
|
-
} else if (desc.kind === "boolean") {
|
|
208
|
-
flags[name] =
|
|
209
|
-
raw !== undefined ? Boolean(raw) : desc.default !== undefined ? Boolean(desc.default) : undefined;
|
|
210
|
-
} else {
|
|
211
|
-
// string
|
|
212
|
-
const val = raw !== undefined && typeof raw !== "boolean" ? raw : (desc.default ?? undefined);
|
|
213
|
-
// Validate options constraint
|
|
214
|
-
if (val !== undefined && desc.options && !Array.isArray(val)) {
|
|
215
|
-
if (!desc.options.includes(val as string)) {
|
|
216
|
-
throw new CliUsageError(
|
|
217
|
-
`Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
flags[name] = val;
|
|
222
|
-
}
|
|
223
|
-
// Validate required
|
|
224
|
-
if (desc.required && flags[name] === undefined) {
|
|
225
|
-
throw new CliUsageError(`Missing required flag: --${name}`);
|
|
226
|
-
}
|
|
171
|
+
/** Parse command argv from lightweight syntax metadata without loading its implementation. */
|
|
172
|
+
export function parseCommandArgv(argv: readonly string[], command: CommandSyntax): CommandParseResult {
|
|
173
|
+
const Cmd = command;
|
|
174
|
+
const flagDefs = (Cmd.flags ?? {}) as Record<string, FlagDescriptor>;
|
|
175
|
+
const argDefs = (Cmd.args ?? {}) as Record<string, ArgDescriptor>;
|
|
176
|
+
const strict = Cmd.strict !== false;
|
|
177
|
+
|
|
178
|
+
// Build node:util parseArgs options from flag descriptors
|
|
179
|
+
const options: Record<
|
|
180
|
+
string,
|
|
181
|
+
{ type: "string" | "boolean"; short?: string; multiple?: boolean; default?: string | boolean }
|
|
182
|
+
> = {};
|
|
183
|
+
for (const [name, desc] of Object.entries(flagDefs)) {
|
|
184
|
+
const opt: (typeof options)[string] = {
|
|
185
|
+
type: desc.kind === "boolean" ? "boolean" : "string",
|
|
186
|
+
};
|
|
187
|
+
if (desc.char) opt.short = desc.char;
|
|
188
|
+
if (desc.multiple) opt.multiple = true;
|
|
189
|
+
if (desc.default !== undefined) {
|
|
190
|
+
opt.default = desc.kind === "boolean" ? Boolean(desc.default) : String(desc.default);
|
|
227
191
|
}
|
|
192
|
+
options[name] = opt;
|
|
193
|
+
}
|
|
228
194
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
195
|
+
// strict=false when command declares args (positionals must pass through)
|
|
196
|
+
// or when the command itself opts out
|
|
197
|
+
const { values: rawValues, positionals } = (() => {
|
|
198
|
+
try {
|
|
199
|
+
return nodeParseArgs({
|
|
200
|
+
args: [...argv],
|
|
201
|
+
options,
|
|
202
|
+
allowPositionals: true,
|
|
203
|
+
strict,
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error instanceof TypeError) throw new CliUsageError(error.message);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
})();
|
|
210
|
+
|
|
211
|
+
// Convert raw values to proper types and validate
|
|
212
|
+
const flags: Record<string, unknown> = {};
|
|
213
|
+
for (const [name, desc] of Object.entries(flagDefs)) {
|
|
214
|
+
const raw = rawValues[name];
|
|
215
|
+
if (desc.kind === "integer") {
|
|
216
|
+
if (raw === undefined || typeof raw === "boolean") {
|
|
217
|
+
flags[name] = desc.default ?? undefined;
|
|
237
218
|
} else {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (desc.required && args[argName] === undefined) {
|
|
244
|
-
throw new CliUsageError(`Missing required argument: ${argName}`);
|
|
219
|
+
const n = Number.parseInt(raw as string, 10);
|
|
220
|
+
if (Number.isNaN(n)) {
|
|
221
|
+
throw new CliUsageError(`Expected integer for --${name}, got "${raw}"`);
|
|
222
|
+
}
|
|
223
|
+
flags[name] = n;
|
|
245
224
|
}
|
|
225
|
+
} else if (desc.kind === "boolean") {
|
|
226
|
+
flags[name] =
|
|
227
|
+
raw !== undefined ? Boolean(raw) : desc.default !== undefined ? Boolean(desc.default) : undefined;
|
|
228
|
+
} else {
|
|
229
|
+
// string
|
|
230
|
+
const val = raw !== undefined && typeof raw !== "boolean" ? raw : (desc.default ?? undefined);
|
|
246
231
|
// Validate options constraint
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!desc.options.includes(argVal)) {
|
|
232
|
+
if (val !== undefined && desc.options && !Array.isArray(val)) {
|
|
233
|
+
if (!desc.options.includes(val as string)) {
|
|
250
234
|
throw new CliUsageError(
|
|
251
|
-
`Expected
|
|
235
|
+
`Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`,
|
|
252
236
|
);
|
|
253
237
|
}
|
|
254
238
|
}
|
|
239
|
+
flags[name] = val;
|
|
240
|
+
}
|
|
241
|
+
// Validate required
|
|
242
|
+
if (desc.required && flags[name] === undefined) {
|
|
243
|
+
throw new CliUsageError(`Missing required flag: --${name}`);
|
|
255
244
|
}
|
|
245
|
+
}
|
|
256
246
|
|
|
257
|
-
|
|
247
|
+
// Map positionals to named args in declaration order and validate
|
|
248
|
+
const args: Record<string, unknown> = {};
|
|
249
|
+
let posIdx = 0;
|
|
250
|
+
for (const [argName, desc] of Object.entries(argDefs)) {
|
|
251
|
+
if (desc.multiple) {
|
|
252
|
+
const val = positionals.slice(posIdx);
|
|
253
|
+
args[argName] = val.length > 0 ? val : undefined;
|
|
254
|
+
posIdx = positionals.length;
|
|
255
|
+
} else {
|
|
256
|
+
const val = positionals[posIdx];
|
|
257
|
+
args[argName] = val;
|
|
258
|
+
posIdx++;
|
|
259
|
+
}
|
|
260
|
+
// Validate required
|
|
261
|
+
if (desc.required && args[argName] === undefined) {
|
|
262
|
+
throw new CliUsageError(`Missing required argument: ${argName}`);
|
|
263
|
+
}
|
|
264
|
+
// Validate options constraint
|
|
265
|
+
const argVal = args[argName];
|
|
266
|
+
if (argVal !== undefined && desc.options && typeof argVal === "string") {
|
|
267
|
+
if (!desc.options.includes(argVal)) {
|
|
268
|
+
throw new CliUsageError(
|
|
269
|
+
`Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
258
273
|
}
|
|
274
|
+
|
|
275
|
+
return { flags, args, argv: positionals };
|
|
259
276
|
}
|
|
260
277
|
|
|
261
278
|
// ---------------------------------------------------------------------------
|
|
@@ -361,6 +378,10 @@ export interface CommandEntry {
|
|
|
361
378
|
name: string;
|
|
362
379
|
load: () => Promise<CommandCtor>;
|
|
363
380
|
aliases?: string[];
|
|
381
|
+
/** Optional command-specific validation that runs before generic syntax parsing. */
|
|
382
|
+
validate?: (argv: readonly string[]) => void;
|
|
383
|
+
/** Optional leaf metadata used to reject invalid argv before loading a heavy command module. */
|
|
384
|
+
syntax?: CommandSyntax;
|
|
364
385
|
}
|
|
365
386
|
|
|
366
387
|
export interface RunOptions {
|
|
@@ -429,10 +450,12 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
429
450
|
return;
|
|
430
451
|
}
|
|
431
452
|
|
|
432
|
-
const Cmd = await entry.load();
|
|
433
|
-
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
434
|
-
const instance = new Cmd(commandArgv, config);
|
|
435
453
|
try {
|
|
454
|
+
entry.validate?.(commandArgv);
|
|
455
|
+
if (entry.syntax) parseCommandArgv(commandArgv, entry.syntax);
|
|
456
|
+
const Cmd = await entry.load();
|
|
457
|
+
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
458
|
+
const instance = new Cmd(commandArgv, config);
|
|
436
459
|
await instance.run();
|
|
437
460
|
} catch (error) {
|
|
438
461
|
if (!(error instanceof CliUsageError)) throw error;
|