hanzio 3.1.1 → 3.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/README.md +8 -1
- package/dist/cli/fields.d.ts +16 -6
- package/dist/cli/index.d.ts +57 -4
- package/dist/crypto/node.d.ts +17 -0
- package/dist/crypto/sealer.d.ts +34 -2
- package/dist/format/index.d.ts +6 -3
- package/dist/{index-nfvq3zax.js → index-818f7jz4.js} +228 -88
- package/dist/index-zz773050.js +135 -0
- package/dist/redis/concurrency-limit.d.ts +17 -7
- package/dist/src/cli/index.js +3 -1
- package/dist/src/crypto/index.js +15 -106
- package/dist/src/crypto/node.js +67 -0
- package/dist/src/format/index.js +4 -3
- package/dist/src/i18n/cli.js +33 -1
- package/dist/src/redis/index.js +5 -1
- package/dist/src/testing/index.js +23 -4
- package/dist/string/index.d.ts +1 -1
- package/dist/testing/module-mocker.d.ts +9 -9
- package/dist/testing/prisma.d.ts +5 -1
- package/dist/typedswitch/index.d.ts +26 -1
- package/package.json +8 -2
- package/src/cli/README.md +63 -6
- package/src/cli/fields.ts +95 -38
- package/src/cli/index.ts +302 -91
- package/src/crypto/README.md +21 -7
- package/src/crypto/node.ts +90 -0
- package/src/crypto/sealer.ts +120 -62
- package/src/format/README.md +7 -0
- package/src/format/index.ts +11 -5
- package/src/i18n/README.md +3 -1
- package/src/i18n/cli.ts +40 -0
- package/src/redis/README.md +9 -7
- package/src/redis/concurrency-limit.ts +26 -8
- package/src/string/index.ts +1 -1
- package/src/testing/README.md +12 -3
- package/src/testing/module-mocker.ts +40 -10
- package/src/testing/prisma.ts +6 -1
- package/src/typedswitch/README.md +20 -9
- package/src/typedswitch/index.ts +84 -8
|
@@ -7,8 +7,38 @@ import {
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
|
|
9
9
|
// src/cli/fields.ts
|
|
10
|
-
import { z } from "zod";
|
|
11
10
|
class UsageError extends Error {
|
|
11
|
+
kind;
|
|
12
|
+
constructor(message, kind = "invalid-arguments") {
|
|
13
|
+
super(message);
|
|
14
|
+
this.kind = kind;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
var optionName = (field) => field.flag.startsWith("no-") ? field.key : field.flag;
|
|
18
|
+
var strings = (value, what, key) => {
|
|
19
|
+
const list = value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
20
|
+
if (list.some((item) => typeof item !== "string" || !item))
|
|
21
|
+
throw new Error(`hanzio/cli: ${what} ${JSON.stringify(value)} of "${key}" must be a string or string[]`);
|
|
22
|
+
return list;
|
|
23
|
+
};
|
|
24
|
+
var kindOf = (def) => def.type === "boolean" ? "boolean" : def.type === "number" ? "number" : "string";
|
|
25
|
+
function choicesOf(def) {
|
|
26
|
+
if (def.type === "enum")
|
|
27
|
+
return Object.values(def.entries).map(String);
|
|
28
|
+
if (def.type === "literal")
|
|
29
|
+
return def.values.map(String);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
function optionalValueOf(def) {
|
|
33
|
+
if (def.type !== "union" || def.options.length !== 2)
|
|
34
|
+
return;
|
|
35
|
+
const [a, b] = def.options.map(defOf);
|
|
36
|
+
const flag = (d) => d.type === "boolean" || d.type === "literal" && d.values.includes(true);
|
|
37
|
+
if (flag(a) && kindOf(b) === "string" && b.type !== "literal")
|
|
38
|
+
return b;
|
|
39
|
+
if (flag(b) && kindOf(a) === "string" && a.type !== "literal")
|
|
40
|
+
return a;
|
|
41
|
+
return;
|
|
12
42
|
}
|
|
13
43
|
var kebab = (key) => key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
14
44
|
function fieldsOf(schema) {
|
|
@@ -23,7 +53,7 @@ function toField(key, schema) {
|
|
|
23
53
|
let defaultValue;
|
|
24
54
|
let current = schema;
|
|
25
55
|
for (;; ) {
|
|
26
|
-
const meta =
|
|
56
|
+
const meta = current.meta?.();
|
|
27
57
|
if (meta)
|
|
28
58
|
metas.unshift(meta);
|
|
29
59
|
const def = defOf(current);
|
|
@@ -49,22 +79,23 @@ function toField(key, schema) {
|
|
|
49
79
|
}
|
|
50
80
|
def = defOf(element);
|
|
51
81
|
}
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
throw new Error(`hanzio/cli: alias ${JSON.stringify(alias)} of "${key}" must be one character`);
|
|
82
|
+
const valueDef = multiple ? undefined : optionalValueOf(def);
|
|
83
|
+
const names = strings(meta.alias, "alias", key);
|
|
55
84
|
return {
|
|
56
85
|
key,
|
|
57
86
|
flag: kebab(key),
|
|
58
|
-
kind:
|
|
87
|
+
kind: valueDef ? "string" : kindOf(def),
|
|
59
88
|
multiple,
|
|
60
89
|
positional: meta.positional === true,
|
|
61
90
|
optional,
|
|
62
91
|
hasDefault,
|
|
63
92
|
defaultValue,
|
|
64
93
|
description: typeof meta.description === "string" ? meta.description : undefined,
|
|
65
|
-
alias,
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
alias: names.find((name) => name.length === 1),
|
|
95
|
+
aliases: names.filter((name) => name.length > 1).map(kebab),
|
|
96
|
+
env: strings(meta.env, "env", key),
|
|
97
|
+
optionalValue: valueDef !== undefined,
|
|
98
|
+
choices: choicesOf(valueDef ?? def)
|
|
68
99
|
};
|
|
69
100
|
}
|
|
70
101
|
function coerce(field, value) {
|
|
@@ -82,7 +113,7 @@ function coerce(field, value) {
|
|
|
82
113
|
}
|
|
83
114
|
return value;
|
|
84
115
|
}
|
|
85
|
-
function readFields(fields, values, positionals, env) {
|
|
116
|
+
function readFields(fields, values, positionals, env, strict = true) {
|
|
86
117
|
const result = {};
|
|
87
118
|
const queue = [...positionals];
|
|
88
119
|
for (const field of fields) {
|
|
@@ -90,11 +121,12 @@ function readFields(fields, values, positionals, env) {
|
|
|
90
121
|
if (field.positional)
|
|
91
122
|
raw = field.multiple ? queue.length ? queue.splice(0) : undefined : queue.shift();
|
|
92
123
|
else
|
|
93
|
-
raw = values[field
|
|
94
|
-
if (
|
|
95
|
-
|
|
124
|
+
raw = values[optionName(field)];
|
|
125
|
+
if (field.optionalValue && raw === "")
|
|
126
|
+
raw = true;
|
|
127
|
+
const value = raw === undefined ? field.env.map((name) => env[name]).find((value) => value?.trim()) : undefined;
|
|
128
|
+
if (value !== undefined)
|
|
96
129
|
raw = field.multiple ? [value] : value;
|
|
97
|
-
}
|
|
98
130
|
if (raw === undefined) {
|
|
99
131
|
if (field.optional || field.hasDefault)
|
|
100
132
|
continue;
|
|
@@ -107,23 +139,25 @@ function readFields(fields, values, positionals, env) {
|
|
|
107
139
|
}
|
|
108
140
|
result[field.key] = Array.isArray(raw) ? raw.map((item) => coerce(field, item)) : coerce(field, raw);
|
|
109
141
|
}
|
|
110
|
-
if (queue.length)
|
|
142
|
+
if (strict && queue.length)
|
|
111
143
|
throw new UsageError(`Unexpected argument "${queue[0]}"`);
|
|
112
144
|
return result;
|
|
113
145
|
}
|
|
114
146
|
function fieldLabel(field) {
|
|
115
147
|
if (field.positional)
|
|
116
148
|
return field.multiple ? `<${field.flag}...>` : field.optional || field.hasDefault ? `[${field.flag}]` : `<${field.flag}>`;
|
|
117
|
-
const name = field.kind === "boolean" && field.defaultValue === true ? `--[no-]${field.flag}` : `--${field.flag}`;
|
|
118
|
-
const
|
|
119
|
-
|
|
149
|
+
const name = field.kind === "boolean" && field.defaultValue === true && !field.flag.startsWith("no-") ? `--[no-]${field.flag}` : `--${field.flag}`;
|
|
150
|
+
const type = `<${field.choices?.join("|") ?? field.kind}>`;
|
|
151
|
+
const value = field.kind === "boolean" ? "" : field.optionalValue ? `[=${type}]` : ` ${type}${field.multiple ? "..." : ""}`;
|
|
152
|
+
const aliases = field.aliases.map((alias) => `, --${alias}`).join("");
|
|
153
|
+
return `${field.alias ? `-${field.alias}, ` : ""}${name}${aliases}${value}`;
|
|
120
154
|
}
|
|
121
155
|
function fieldHelp(field) {
|
|
122
156
|
const showDefault = field.hasDefault && field.defaultValue !== undefined && !(field.kind === "boolean" && field.defaultValue === false);
|
|
123
157
|
return [
|
|
124
158
|
field.description,
|
|
125
159
|
showDefault && `(default: ${JSON.stringify(field.defaultValue)})`,
|
|
126
|
-
field.env && `[env: ${field.env}]`
|
|
160
|
+
field.env.length > 0 && `[env: ${field.env.join(", ")}]`
|
|
127
161
|
].filter(Boolean).join(" ");
|
|
128
162
|
}
|
|
129
163
|
function closest(input, candidates) {
|
|
@@ -160,6 +194,19 @@ class CliError2 extends Error {
|
|
|
160
194
|
this.exitCode = options?.exitCode ?? 1;
|
|
161
195
|
}
|
|
162
196
|
}
|
|
197
|
+
|
|
198
|
+
class CliUsageError2 extends CliError2 {
|
|
199
|
+
kind;
|
|
200
|
+
path;
|
|
201
|
+
help;
|
|
202
|
+
constructor(message, details) {
|
|
203
|
+
super(message, { exitCode: 2 });
|
|
204
|
+
this.name = "CliUsageError";
|
|
205
|
+
this.kind = details.kind;
|
|
206
|
+
this.path = details.path;
|
|
207
|
+
this.help = details.help;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
163
210
|
function defineCommand2(definition) {
|
|
164
211
|
return { ...definition, [commandKey]: true };
|
|
165
212
|
}
|
|
@@ -174,12 +221,14 @@ async function loadCommand(node) {
|
|
|
174
221
|
return loadCommand(command);
|
|
175
222
|
}
|
|
176
223
|
function listCommands(group, prefix = []) {
|
|
177
|
-
return Object.entries(group).flatMap(([name, node]) =>
|
|
178
|
-
|
|
179
|
-
[...prefix, name]
|
|
180
|
-
|
|
181
|
-
]
|
|
182
|
-
|
|
224
|
+
return Object.entries(group).flatMap(([name, node]) => {
|
|
225
|
+
if (typeof node !== "function" && !isCommand(node))
|
|
226
|
+
return listCommands(node, [...prefix, name]);
|
|
227
|
+
const command = typeof node === "function" ? undefined : node;
|
|
228
|
+
const names = [...prefix, name, command?.usage].filter(Boolean).join(" ");
|
|
229
|
+
return [[names, command?.description?.split(`
|
|
230
|
+
`)[0]]];
|
|
231
|
+
});
|
|
183
232
|
}
|
|
184
233
|
function table(rows) {
|
|
185
234
|
const width = Math.max(...rows.map(([left]) => left.length));
|
|
@@ -190,14 +239,20 @@ var optionRows = (fields) => fields.map((field) => [fieldLabel(field), fieldHelp
|
|
|
190
239
|
var helpRow = ["-h, --help", "Show help"];
|
|
191
240
|
function defineCli2(config) {
|
|
192
241
|
const root = config.commands;
|
|
242
|
+
const unknownCommand = config.unknownCommand;
|
|
193
243
|
const builtIn = (group) => group === root && !Object.hasOwn(group, "completion");
|
|
194
244
|
const versionRow = config.version ? [["--version", "Show version"]] : [];
|
|
245
|
+
const section = (title, fields) => fields.length ? [`${title}:
|
|
246
|
+
${table(optionRows(fields))}`] : [];
|
|
247
|
+
const positionalsOf = (fields) => fields.filter((field) => field.positional);
|
|
248
|
+
const optionsOf = (fields) => fields.filter((field) => !field.positional);
|
|
195
249
|
function groupHelp(group, path, globals) {
|
|
196
|
-
const usage = [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
]);
|
|
250
|
+
const usage = [
|
|
251
|
+
config.name,
|
|
252
|
+
...positionalsOf(globals).map(fieldLabel),
|
|
253
|
+
...path
|
|
254
|
+
].join(" ");
|
|
255
|
+
const commands = listCommands(group);
|
|
201
256
|
if (builtIn(group))
|
|
202
257
|
commands.push([
|
|
203
258
|
"completion <bash|zsh>",
|
|
@@ -210,13 +265,15 @@ function defineCli2(config) {
|
|
|
210
265
|
""
|
|
211
266
|
],
|
|
212
267
|
`Usage: ${usage} <command> [options]`,
|
|
268
|
+
...section(`
|
|
269
|
+
Arguments`, positionalsOf(globals)),
|
|
213
270
|
"",
|
|
214
271
|
"Commands:",
|
|
215
272
|
table(commands),
|
|
216
273
|
"",
|
|
217
274
|
"Options:",
|
|
218
275
|
table([
|
|
219
|
-
...optionRows(globals),
|
|
276
|
+
...optionRows(optionsOf(globals)),
|
|
220
277
|
helpRow,
|
|
221
278
|
...path.length ? [] : versionRow
|
|
222
279
|
]),
|
|
@@ -225,34 +282,42 @@ function defineCli2(config) {
|
|
|
225
282
|
].join(`
|
|
226
283
|
`);
|
|
227
284
|
}
|
|
228
|
-
function commandHelp(command, path,
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
285
|
+
function commandHelp(command, path, globals) {
|
|
286
|
+
const fields = command.raw ? [] : fieldsOf(command.input);
|
|
287
|
+
const positionals = [...positionalsOf(globals), ...positionalsOf(fields)];
|
|
288
|
+
const usage = [
|
|
289
|
+
config.name,
|
|
290
|
+
...positionalsOf(globals).map(fieldLabel),
|
|
291
|
+
...path,
|
|
292
|
+
command.usage ?? [...positionalsOf(fields).map(fieldLabel), "[options]"]
|
|
233
293
|
];
|
|
294
|
+
const sections = [`Usage: ${usage.flat().join(" ")}`];
|
|
234
295
|
const description = command.description ?? config.description;
|
|
235
296
|
if (description)
|
|
236
297
|
sections.push(description);
|
|
237
|
-
|
|
238
|
-
sections.push(`Arguments:
|
|
239
|
-
${table(optionRows(positionals))}`);
|
|
298
|
+
sections.push(...section("Arguments", positionals));
|
|
240
299
|
sections.push(`Options:
|
|
241
300
|
${table([
|
|
242
|
-
...optionRows(
|
|
301
|
+
...optionRows(optionsOf(fields)),
|
|
243
302
|
helpRow,
|
|
244
303
|
...path.length ? [] : versionRow
|
|
245
304
|
])}`);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
305
|
+
sections.push(...section("Global options", optionsOf(globals)));
|
|
306
|
+
if (command.examples?.length)
|
|
307
|
+
sections.push(`Examples:
|
|
308
|
+
${command.examples.map((line) => ` ${line}`).join(`
|
|
309
|
+
`)}`);
|
|
249
310
|
return sections.join(`
|
|
250
311
|
|
|
251
312
|
`);
|
|
252
313
|
}
|
|
314
|
+
async function helpText(node, path, globals) {
|
|
315
|
+
const text = isCommand(node) ? commandHelp(node, path, globals) : groupHelp(node, path, globals);
|
|
316
|
+
return config.help ? config.help(text, { path }) : text;
|
|
317
|
+
}
|
|
253
318
|
async function completion(shell, globals) {
|
|
254
319
|
const fn = `_${config.name.replace(/\W/g, "_")}_completion`;
|
|
255
|
-
const flags = (fields) => [...fields, ...globals]
|
|
320
|
+
const flags = (fields) => optionsOf([...fields, ...globals]).map((field) => `--${field.flag}`);
|
|
256
321
|
const cases = [];
|
|
257
322
|
const visit = async (node, path) => {
|
|
258
323
|
if (typeof node === "function" || isCommand(node)) {
|
|
@@ -301,69 +366,112 @@ ${script}`;
|
|
|
301
366
|
const env = io.env ?? process.env;
|
|
302
367
|
let usage = config.name;
|
|
303
368
|
let json = false;
|
|
369
|
+
let globalFields = [];
|
|
370
|
+
let current = {
|
|
371
|
+
node: {},
|
|
372
|
+
path: []
|
|
373
|
+
};
|
|
304
374
|
try {
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
375
|
+
globalFields = fieldsOf(config.globals);
|
|
376
|
+
const globalPositionals = positionalsOf(globalFields);
|
|
377
|
+
const array = globalPositionals.find((field) => field.multiple);
|
|
378
|
+
if (array)
|
|
379
|
+
throw new Error(`hanzio/cli: positional global "${array.key}" can't be an array`);
|
|
380
|
+
const longNames = (field) => [
|
|
310
381
|
`--${field.flag}`,
|
|
382
|
+
...field.aliases.map((alias) => `--${alias}`)
|
|
383
|
+
];
|
|
384
|
+
const names = (field) => [
|
|
385
|
+
...longNames(field),
|
|
311
386
|
...field.alias ? [`-${field.alias}`] : []
|
|
387
|
+
];
|
|
388
|
+
const globalOptions = optionsOf(globalFields);
|
|
389
|
+
const valueFlags = new Set(globalOptions.filter((field) => field.kind !== "boolean" && !field.optionalValue).flatMap(names));
|
|
390
|
+
const globalFlags = new Set(globalOptions.flatMap((field) => [
|
|
391
|
+
...names(field),
|
|
392
|
+
`--no-${field.flag}`
|
|
312
393
|
]));
|
|
313
394
|
const isHelp = (token) => token === "--help" || token === "-h";
|
|
395
|
+
const isVersion = (token) => token === "--version" || token === "-v" && !globalOptions.some((field) => field.alias === "v");
|
|
396
|
+
const isFlag = (token) => token.startsWith("-") && token !== "-";
|
|
397
|
+
const args = [...argv];
|
|
398
|
+
let defaultCommand = typeof config.defaultCommand === "function" ? config.defaultCommand(env) : config.defaultCommand;
|
|
399
|
+
let injected = -1;
|
|
314
400
|
let node = root;
|
|
315
401
|
const path = [];
|
|
316
402
|
const pre = [];
|
|
403
|
+
const positionals = [];
|
|
317
404
|
let index = 0;
|
|
318
405
|
while (typeof node !== "function" && !isCommand(node)) {
|
|
319
406
|
const group = node;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
407
|
+
current = { node: group, path: [...path] };
|
|
408
|
+
const token = args[index];
|
|
409
|
+
const end = token === undefined || token === "--";
|
|
410
|
+
if (end && pre.some(isHelp)) {
|
|
411
|
+
stdout.write(`${await helpText(group, path, globalFields)}
|
|
324
412
|
`);
|
|
325
|
-
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
if (end && !path.length && pre.some(isVersion)) {
|
|
416
|
+
if (Object.hasOwn(group, "version")) {
|
|
417
|
+
pre.splice(0, pre.length, ...pre.filter((t) => !isVersion(t)));
|
|
418
|
+
args.splice(index, 0, "version");
|
|
419
|
+
injected = index;
|
|
420
|
+
continue;
|
|
326
421
|
}
|
|
327
|
-
if (
|
|
422
|
+
if (config.version) {
|
|
328
423
|
stdout.write(`${config.version}
|
|
329
424
|
`);
|
|
330
425
|
return 0;
|
|
331
426
|
}
|
|
427
|
+
}
|
|
428
|
+
if (!path.length && defaultCommand !== undefined && (end || isFlag(token) && !isHelp(token) && !isVersion(token) && !globalFlags.has(token.split("=")[0]))) {
|
|
429
|
+
args.splice(index, 0, defaultCommand);
|
|
430
|
+
injected = index;
|
|
431
|
+
defaultCommand = undefined;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (end)
|
|
332
435
|
throw new UsageError(`${path.length ? `${usage}: missing` : "Missing"} command
|
|
333
436
|
|
|
334
|
-
${
|
|
335
|
-
}
|
|
437
|
+
${await helpText(group, path, globalFields)}`, "missing-command");
|
|
336
438
|
index++;
|
|
337
|
-
if (
|
|
439
|
+
if (isFlag(token)) {
|
|
338
440
|
pre.push(token);
|
|
339
|
-
if (valueFlags.has(token) &&
|
|
340
|
-
pre.push(
|
|
441
|
+
if (valueFlags.has(token) && args[index] !== undefined)
|
|
442
|
+
pre.push(args[index++]);
|
|
341
443
|
continue;
|
|
342
444
|
}
|
|
343
|
-
if (token === "completion" && builtIn(group)) {
|
|
344
|
-
const [shell = "bash"] =
|
|
445
|
+
if (token === "completion" && builtIn(group) && !positionals.length) {
|
|
446
|
+
const [shell = "bash"] = args.slice(index);
|
|
345
447
|
stdout.write(await completion(shell, globalFields));
|
|
346
448
|
return 0;
|
|
347
449
|
}
|
|
348
|
-
|
|
450
|
+
if (!path.length && index - 1 !== injected && positionals.length < globalPositionals.length) {
|
|
451
|
+
positionals.push(token);
|
|
452
|
+
usage = `${usage} ${token}`;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const child = (Object.hasOwn(group, token) ? group[token] : undefined) ?? await unknownCommand?.(token, path);
|
|
349
456
|
if (!child) {
|
|
350
457
|
const names = Object.keys(group);
|
|
351
458
|
if (builtIn(group))
|
|
352
459
|
names.push("completion");
|
|
353
460
|
const guess = closest(token, names);
|
|
354
|
-
throw new UsageError(`Unknown command "${[...path, token].join(" ")}".${guess ? ` Did you mean "${[...path, guess].join(" ")}"?` : ` Commands: ${names.join(", ")}`}
|
|
461
|
+
throw new UsageError(`Unknown command "${[...path, token].join(" ")}".${guess ? ` Did you mean "${[...path, guess].join(" ")}"?` : ` Commands: ${names.join(", ")}`}`, "unknown-command");
|
|
355
462
|
}
|
|
356
463
|
node = child;
|
|
357
464
|
path.push(token);
|
|
358
465
|
usage = `${usage} ${token}`;
|
|
359
466
|
}
|
|
360
467
|
const command = await loadCommand(node);
|
|
361
|
-
|
|
468
|
+
current = { node: command, path };
|
|
469
|
+
const rest = args.slice(index);
|
|
362
470
|
const fields = command.raw ? [] : fieldsOf(command.input);
|
|
363
471
|
const dash = rest.indexOf("--");
|
|
364
472
|
const tokens = command.raw ? pre : [...pre, ...dash < 0 ? rest : rest.slice(0, dash)];
|
|
365
473
|
if (!command.raw && tokens.some(isHelp)) {
|
|
366
|
-
stdout.write(`${
|
|
474
|
+
stdout.write(`${await helpText(command, path, globalFields)}
|
|
367
475
|
`);
|
|
368
476
|
return 0;
|
|
369
477
|
}
|
|
@@ -373,37 +481,50 @@ ${groupHelp(group, path, globalFields)}`);
|
|
|
373
481
|
return 0;
|
|
374
482
|
}
|
|
375
483
|
const options = {};
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
484
|
+
const canonical = new Map;
|
|
485
|
+
const all = optionsOf([...globalFields, ...fields]);
|
|
486
|
+
for (const field of all) {
|
|
487
|
+
const name = optionName(field);
|
|
488
|
+
for (const long of longNames(field)) {
|
|
489
|
+
if (long === "--help" || canonical.has(long))
|
|
490
|
+
throw new Error(`hanzio/cli: ${long} is declared twice`);
|
|
491
|
+
canonical.set(long, field);
|
|
492
|
+
}
|
|
493
|
+
options[name] = {
|
|
382
494
|
type: field.kind === "boolean" ? "boolean" : "string",
|
|
383
495
|
multiple: field.multiple,
|
|
384
496
|
...field.alias ? { short: field.alias } : {}
|
|
385
497
|
};
|
|
386
498
|
}
|
|
499
|
+
const rewritten = tokens.map((token) => {
|
|
500
|
+
const eq = token.indexOf("=");
|
|
501
|
+
const field = canonical.get(eq < 0 ? token : token.slice(0, eq));
|
|
502
|
+
if (!field)
|
|
503
|
+
return token;
|
|
504
|
+
const value = eq < 0 ? field.optionalValue ? "=" : "" : token.slice(eq);
|
|
505
|
+
return `--${optionName(field)}${value}`;
|
|
506
|
+
});
|
|
507
|
+
const strict = command.strict !== false;
|
|
387
508
|
let parsed;
|
|
388
509
|
try {
|
|
389
510
|
parsed = parseArgs({
|
|
390
|
-
args:
|
|
511
|
+
args: rewritten,
|
|
391
512
|
options,
|
|
392
513
|
allowPositionals: true,
|
|
393
514
|
allowNegative: true,
|
|
394
|
-
strict
|
|
515
|
+
strict
|
|
395
516
|
});
|
|
396
517
|
} catch (error) {
|
|
397
518
|
const message = error instanceof Error ? error.message : String(error);
|
|
398
519
|
const unknown = /^Unknown option '(-[^']*)'/.exec(message)?.[1];
|
|
399
520
|
if (!unknown)
|
|
400
521
|
throw new UsageError(message.split(". ")[0] ?? message);
|
|
401
|
-
const guess = closest(unknown.replace(/^--?/, ""),
|
|
522
|
+
const guess = closest(unknown.replace(/^--?/, ""), all.flatMap((field) => [field.flag, ...field.aliases]));
|
|
402
523
|
throw new UsageError(`Unknown option ${unknown}.${guess ? ` Did you mean --${guess}?` : ""}`);
|
|
403
524
|
}
|
|
404
525
|
const issues = [];
|
|
405
526
|
const validate = async (schema, fields, positionals) => {
|
|
406
|
-
const raw = readFields(fields, parsed.values, positionals, env);
|
|
527
|
+
const raw = readFields(fields, parsed.values, positionals, env, strict);
|
|
407
528
|
if (!schema)
|
|
408
529
|
return raw;
|
|
409
530
|
const result = await schema.safeParseAsync(raw);
|
|
@@ -416,7 +537,7 @@ ${groupHelp(group, path, globalFields)}`);
|
|
|
416
537
|
}
|
|
417
538
|
return raw;
|
|
418
539
|
};
|
|
419
|
-
const globals = await validate(config.globals, globalFields,
|
|
540
|
+
const globals = await validate(config.globals, globalFields, positionals);
|
|
420
541
|
const input = await validate(command.raw ? undefined : command.input, fields, parsed.positionals);
|
|
421
542
|
if (issues.length)
|
|
422
543
|
throw new UsageError(issues.join(`
|
|
@@ -447,20 +568,39 @@ ${groupHelp(group, path, globalFields)}`);
|
|
|
447
568
|
await output(result, ctx);
|
|
448
569
|
return ctx.exitCode;
|
|
449
570
|
} catch (error) {
|
|
450
|
-
|
|
451
|
-
|
|
571
|
+
const failure = error instanceof UsageError ? new CliUsageError2(error.message, {
|
|
572
|
+
kind: error.kind,
|
|
573
|
+
path: current.path,
|
|
574
|
+
help: config.onError ? `${await helpText(current.node, current.path, globalFields)}
|
|
575
|
+
` : ""
|
|
576
|
+
}) : error;
|
|
577
|
+
let printed;
|
|
578
|
+
const print = () => {
|
|
579
|
+
if (printed !== undefined)
|
|
580
|
+
return printed;
|
|
581
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
582
|
+
if (failure instanceof CliUsageError2)
|
|
583
|
+
stderr.write(`${message}
|
|
452
584
|
Run "${usage} --help" for usage.
|
|
453
585
|
`);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
457
|
-
if (json)
|
|
458
|
-
stderr.write(`${JSON.stringify({ error: message })}
|
|
586
|
+
else if (json)
|
|
587
|
+
stderr.write(`${JSON.stringify({ error: message })}
|
|
459
588
|
`);
|
|
460
|
-
|
|
461
|
-
|
|
589
|
+
else
|
|
590
|
+
stderr.write(`${failure instanceof Error && !(failure instanceof CliError2) ? failure.stack ?? message : message}
|
|
462
591
|
`);
|
|
463
|
-
|
|
592
|
+
printed = failure instanceof CliError2 ? failure.exitCode : 1;
|
|
593
|
+
return printed;
|
|
594
|
+
};
|
|
595
|
+
if (!config.onError)
|
|
596
|
+
return print();
|
|
597
|
+
return await config.onError(failure, {
|
|
598
|
+
stdout,
|
|
599
|
+
stderr,
|
|
600
|
+
env,
|
|
601
|
+
json,
|
|
602
|
+
print
|
|
603
|
+
}) ?? print();
|
|
464
604
|
}
|
|
465
605
|
}
|
|
466
606
|
};
|
|
@@ -472,4 +612,4 @@ function printResult(result, ctx) {
|
|
|
472
612
|
`);
|
|
473
613
|
}
|
|
474
614
|
|
|
475
|
-
export { CliError2, defineCommand2, defineCli2 };
|
|
615
|
+
export { CliError2, CliUsageError2, defineCommand2, defineCli2 };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
utf8Encode2,
|
|
3
|
+
utf8Decode2,
|
|
4
|
+
encodeBase64Url2,
|
|
5
|
+
decodeBase64Url2,
|
|
6
|
+
toBytes,
|
|
7
|
+
randomBytes2,
|
|
8
|
+
getSubtleCrypto
|
|
9
|
+
} from "./index-rp5pqghz.js";
|
|
10
|
+
|
|
11
|
+
// src/crypto/sealer.ts
|
|
12
|
+
var ERROR_MESSAGES = {
|
|
13
|
+
malformed: "Malformed sealed value",
|
|
14
|
+
unsupported_version: "Unsupported sealed value version",
|
|
15
|
+
decrypt_failed: "Could not open sealed value: wrong key or tampered data"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class SealerError2 extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code) {
|
|
21
|
+
super(ERROR_MESSAGES[code]);
|
|
22
|
+
this.name = "SealerError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var PREFIX = "enc:v1:";
|
|
27
|
+
var IV_BYTES = 12;
|
|
28
|
+
var TAG_BYTES = 16;
|
|
29
|
+
var DEFAULT_INFO = "hanzio.sealer.aes-256-gcm.v1";
|
|
30
|
+
function sealerKeyInputs2(options) {
|
|
31
|
+
const secret = toBytes(options.secret);
|
|
32
|
+
if (secret.length === 0) {
|
|
33
|
+
throw new TypeError("Sealer secret must not be empty");
|
|
34
|
+
}
|
|
35
|
+
const contexts = [options, ...options.fallbacks ?? []].map(({ salt = "", info = DEFAULT_INFO }) => ({ salt, info }));
|
|
36
|
+
return { secret, contexts };
|
|
37
|
+
}
|
|
38
|
+
function formatEnvelope2(iv, tag, ciphertext) {
|
|
39
|
+
return `${PREFIX}${encodeBase64Url2(iv)}.${encodeBase64Url2(tag)}.${encodeBase64Url2(ciphertext)}`;
|
|
40
|
+
}
|
|
41
|
+
function parseEnvelope2(sealed, passthroughUnsealed = false) {
|
|
42
|
+
if (typeof sealed !== "string" || !sealed.startsWith(PREFIX)) {
|
|
43
|
+
const isEnvelope = typeof sealed === "string" && /^enc:v\d+:/.test(sealed);
|
|
44
|
+
if (isEnvelope)
|
|
45
|
+
throw new SealerError2("unsupported_version");
|
|
46
|
+
if (passthroughUnsealed)
|
|
47
|
+
return null;
|
|
48
|
+
throw new SealerError2("malformed");
|
|
49
|
+
}
|
|
50
|
+
const parts = sealed.slice(PREFIX.length).split(".");
|
|
51
|
+
let envelope;
|
|
52
|
+
try {
|
|
53
|
+
if (parts.length !== 3)
|
|
54
|
+
throw new Error;
|
|
55
|
+
envelope = {
|
|
56
|
+
iv: decodeBase64Url2(parts[0]),
|
|
57
|
+
tag: decodeBase64Url2(parts[1]),
|
|
58
|
+
ciphertext: decodeBase64Url2(parts[2])
|
|
59
|
+
};
|
|
60
|
+
} catch (_error) {
|
|
61
|
+
throw new SealerError2("malformed");
|
|
62
|
+
}
|
|
63
|
+
if (envelope.iv.length !== IV_BYTES || envelope.tag.length !== TAG_BYTES) {
|
|
64
|
+
throw new SealerError2("malformed");
|
|
65
|
+
}
|
|
66
|
+
return envelope;
|
|
67
|
+
}
|
|
68
|
+
function jsonToSeal2(value) {
|
|
69
|
+
const json = JSON.stringify(value);
|
|
70
|
+
if (json === undefined) {
|
|
71
|
+
throw new TypeError("sealJson value is not JSON-serializable");
|
|
72
|
+
}
|
|
73
|
+
return json;
|
|
74
|
+
}
|
|
75
|
+
function createSealer2(options) {
|
|
76
|
+
const { secret, contexts } = sealerKeyInputs2(options);
|
|
77
|
+
const keys = [];
|
|
78
|
+
const getKey = (index) => {
|
|
79
|
+
let key = keys[index];
|
|
80
|
+
if (!key) {
|
|
81
|
+
const context = contexts[index];
|
|
82
|
+
key = deriveKey(secret, context.salt, context.info);
|
|
83
|
+
keys[index] = key;
|
|
84
|
+
key.catch(() => {
|
|
85
|
+
keys[index] = undefined;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return key;
|
|
89
|
+
};
|
|
90
|
+
const seal = async (plaintext) => {
|
|
91
|
+
const iv = randomBytes2(IV_BYTES);
|
|
92
|
+
const encrypted = new Uint8Array(await getSubtleCrypto().encrypt({ name: "AES-GCM", iv }, await getKey(0), utf8Encode2(plaintext)));
|
|
93
|
+
const tagStart = encrypted.length - TAG_BYTES;
|
|
94
|
+
return formatEnvelope2(iv, encrypted.subarray(tagStart), encrypted.subarray(0, tagStart));
|
|
95
|
+
};
|
|
96
|
+
const openEnvelope = async ({ iv, tag, ciphertext }) => {
|
|
97
|
+
const encrypted = new Uint8Array(ciphertext.length + TAG_BYTES);
|
|
98
|
+
encrypted.set(ciphertext);
|
|
99
|
+
encrypted.set(tag, ciphertext.length);
|
|
100
|
+
for (let index = 0;index < contexts.length; index++) {
|
|
101
|
+
const key = await getKey(index);
|
|
102
|
+
try {
|
|
103
|
+
return utf8Decode2(await getSubtleCrypto().decrypt({ name: "AES-GCM", iv }, key, encrypted));
|
|
104
|
+
} catch (_error) {}
|
|
105
|
+
}
|
|
106
|
+
throw new SealerError2("decrypt_failed");
|
|
107
|
+
};
|
|
108
|
+
const open = async (sealed) => {
|
|
109
|
+
const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
|
|
110
|
+
return envelope ? openEnvelope(envelope) : sealed;
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
seal,
|
|
114
|
+
open,
|
|
115
|
+
sealJson: async (value) => seal(jsonToSeal2(value)),
|
|
116
|
+
async openJson(sealed) {
|
|
117
|
+
const envelope = parseEnvelope2(sealed, options.passthroughUnsealed);
|
|
118
|
+
return envelope ? JSON.parse(await openEnvelope(envelope)) : sealed;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function deriveKey(secret, salt, info) {
|
|
123
|
+
const subtle = getSubtleCrypto();
|
|
124
|
+
const baseKey = await subtle.importKey("raw", secret, "HKDF", false, [
|
|
125
|
+
"deriveKey"
|
|
126
|
+
]);
|
|
127
|
+
return subtle.deriveKey({
|
|
128
|
+
name: "HKDF",
|
|
129
|
+
hash: "SHA-256",
|
|
130
|
+
salt: utf8Encode2(salt),
|
|
131
|
+
info: utf8Encode2(info)
|
|
132
|
+
}, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { SealerError2, sealerKeyInputs2, formatEnvelope2, parseEnvelope2, jsonToSeal2, createSealer2 };
|