@kaynooo/env-tools 0.4.1 → 0.6.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/LICENSE +1 -1
- package/README.md +3 -2
- package/dist/ev.js +368 -99
- package/dist/redact.js +281 -59
- package/package.json +6 -6
package/dist/redact.js
CHANGED
|
@@ -343,6 +343,26 @@ function printJson(value) {
|
|
|
343
343
|
console.log(JSON.stringify(value, undefined, 2));
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// ../kit/src/text.ts
|
|
347
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
348
|
+
function distance(a, b) {
|
|
349
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
350
|
+
for (const [i, left] of [...a].entries()) {
|
|
351
|
+
const current = [i + 1];
|
|
352
|
+
for (const [j, right] of [...b].entries())
|
|
353
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
354
|
+
previous = current;
|
|
355
|
+
}
|
|
356
|
+
return previous[b.length] ?? 0;
|
|
357
|
+
}
|
|
358
|
+
function closest(name, names) {
|
|
359
|
+
const started = names.find((known) => known.startsWith(name));
|
|
360
|
+
if (started !== undefined)
|
|
361
|
+
return started;
|
|
362
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
363
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
346
366
|
// ../kit/src/args.ts
|
|
347
367
|
function jsonOption(holds) {
|
|
348
368
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
@@ -362,8 +382,8 @@ function fail(message, code = 2, hint) {
|
|
|
362
382
|
}
|
|
363
383
|
|
|
364
384
|
class UsageError extends Exit {
|
|
365
|
-
constructor(message) {
|
|
366
|
-
super(message, 2);
|
|
385
|
+
constructor(message, hint) {
|
|
386
|
+
super(message, 2, hint);
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
function badUsage(message) {
|
|
@@ -385,24 +405,70 @@ class Completing {
|
|
|
385
405
|
var completion = { active: false };
|
|
386
406
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
387
407
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
408
|
+
var FULL_STOP = /\.$/;
|
|
409
|
+
var DASHES = /^-+/;
|
|
388
410
|
function usageLine(usage, colors = paint(process4.stdout)) {
|
|
389
411
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
390
412
|
}
|
|
391
|
-
function helpText(usage, options, colors = paint(process4.stdout)) {
|
|
392
|
-
return [usageLine(usage, colors), "", colors.bold("Options:"), ...optionRows(options, colors)].join(`
|
|
413
|
+
function helpText(usage, options, colors = paint(process4.stdout), { examples = [], replaces } = {}) {
|
|
414
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
393
415
|
`);
|
|
394
416
|
}
|
|
417
|
+
function replacesRows(replaces, colors = paint(process4.stdout)) {
|
|
418
|
+
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
419
|
+
}
|
|
420
|
+
function wantsHelpJson(env = process4.env) {
|
|
421
|
+
return env.KIT_HELP === "json";
|
|
422
|
+
}
|
|
423
|
+
function description(usage, { examples = [], replaces } = {}) {
|
|
424
|
+
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
425
|
+
`);
|
|
426
|
+
const first = lines[0];
|
|
427
|
+
const colon = first.indexOf(": ");
|
|
428
|
+
const prose = lines[lines.indexOf("") + 1] ?? "";
|
|
429
|
+
const fallback = lines.includes("") ? `${prose.charAt(0).toLowerCase()}${prose.slice(1)}`.replace(FULL_STOP, "") : "";
|
|
430
|
+
return {
|
|
431
|
+
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
432
|
+
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
433
|
+
...replaces === undefined ? {} : { replaces },
|
|
434
|
+
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function exampleRows(examples, colors = paint(process4.stdout)) {
|
|
438
|
+
if (examples.length === 0)
|
|
439
|
+
return [];
|
|
440
|
+
return ["", colors.bold("Examples:"), ...examples.map((example) => typeof example === "string" ? ` ${example}` : ` ${example[0]} ${colors.dim(`# ${example[1]}`)}`)];
|
|
441
|
+
}
|
|
395
442
|
function optionRows(options, colors = paint(process4.stdout)) {
|
|
396
443
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
397
444
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
398
445
|
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
399
|
-
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
|
|
446
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
400
447
|
});
|
|
401
448
|
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
402
|
-
return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
449
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
450
|
+
}
|
|
451
|
+
var OUTPUT_OPTIONS = {
|
|
452
|
+
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
453
|
+
plain: { type: "boolean", description: "plain text, no colour or spinner: --color never" }
|
|
454
|
+
};
|
|
455
|
+
function applyOutput(color, plain, env = process4.env) {
|
|
456
|
+
const when = plain === true ? "never" : color ?? "auto";
|
|
457
|
+
if (when === "auto")
|
|
458
|
+
return;
|
|
459
|
+
if (when !== "always" && when !== "never")
|
|
460
|
+
badUsage(`--color takes auto, always or never, not ${when}`);
|
|
461
|
+
if (when === "always") {
|
|
462
|
+
env.FORCE_COLOR = "1";
|
|
463
|
+
delete env.NO_COLOR;
|
|
464
|
+
} else {
|
|
465
|
+
env.NO_COLOR = "1";
|
|
466
|
+
delete env.FORCE_COLOR;
|
|
467
|
+
}
|
|
403
468
|
}
|
|
404
469
|
function parse(args, usage, options, settings = {}) {
|
|
405
|
-
const
|
|
470
|
+
const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
|
|
471
|
+
const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
|
|
406
472
|
if (completion.active)
|
|
407
473
|
throw new Completing(all);
|
|
408
474
|
const separator = settings.rest === true ? args.indexOf("--") : -1;
|
|
@@ -410,12 +476,15 @@ function parse(args, usage, options, settings = {}) {
|
|
|
410
476
|
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
411
477
|
let parsed;
|
|
412
478
|
try {
|
|
413
|
-
parsed = parseArgs({ args: own, options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
479
|
+
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
414
480
|
} catch (error) {
|
|
415
|
-
|
|
481
|
+
const message = parseError(error);
|
|
482
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
416
483
|
}
|
|
417
|
-
|
|
418
|
-
|
|
484
|
+
const values = parsed.values;
|
|
485
|
+
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
486
|
+
if (values.help === true) {
|
|
487
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, all, undefined, settings));
|
|
419
488
|
throw new HelpShown;
|
|
420
489
|
}
|
|
421
490
|
if (settings.positionals !== undefined)
|
|
@@ -430,10 +499,45 @@ function checkPositionals(given, names) {
|
|
|
430
499
|
if (!variadic && given.length > names.length)
|
|
431
500
|
badUsage(`unexpected argument ${given[names.length]}`);
|
|
432
501
|
}
|
|
502
|
+
function joinDashValues(args, options) {
|
|
503
|
+
const longOf = new Map(Object.entries(options).flatMap(([name, option]) => option.short === undefined ? [[`--${name}`, name]] : [[`--${name}`, name], [`-${option.short}`, name]]));
|
|
504
|
+
const isOption = (arg) => arg === "--" || longOf.has(arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg);
|
|
505
|
+
const joined = [];
|
|
506
|
+
for (let i = 0;i < args.length; i++) {
|
|
507
|
+
const arg = args[i] ?? "";
|
|
508
|
+
if (arg === "--")
|
|
509
|
+
return [...joined, ...args.slice(i)];
|
|
510
|
+
const name = longOf.get(arg);
|
|
511
|
+
const next = args[i + 1];
|
|
512
|
+
if (name !== undefined && options[name]?.type === "string" && next !== undefined && next.startsWith("-") && next !== "-" && !isOption(next)) {
|
|
513
|
+
joined.push(`--${name}=${next}`);
|
|
514
|
+
i++;
|
|
515
|
+
} else {
|
|
516
|
+
joined.push(arg);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return joined;
|
|
520
|
+
}
|
|
433
521
|
function parseError(error) {
|
|
434
522
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
435
523
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
436
524
|
}
|
|
525
|
+
var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
|
|
526
|
+
function nearestOption(message, options, allowNegative) {
|
|
527
|
+
const typed = UNKNOWN_OPTION.exec(message)?.[1];
|
|
528
|
+
if (typed === undefined)
|
|
529
|
+
return;
|
|
530
|
+
const word = typed.replace(DASHES, "");
|
|
531
|
+
if (word.length < 2 && !typed.startsWith("--"))
|
|
532
|
+
return;
|
|
533
|
+
const known = Object.entries(options).flatMap(([name, option]) => [
|
|
534
|
+
...option.short === undefined ? [] : [`-${option.short}`],
|
|
535
|
+
`--${name}`,
|
|
536
|
+
...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
|
|
537
|
+
]);
|
|
538
|
+
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
539
|
+
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
540
|
+
}
|
|
437
541
|
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
438
542
|
if (path === "-") {
|
|
439
543
|
if (process4.stdin.isTTY)
|
|
@@ -447,42 +551,75 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
447
551
|
}
|
|
448
552
|
// ../kit/src/cli.ts
|
|
449
553
|
import process5 from "process";
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
554
|
+
var LEVEL_OPTIONS = {
|
|
555
|
+
...OUTPUT_OPTIONS,
|
|
556
|
+
help: { type: "boolean", short: "h", description: "show this help" }
|
|
557
|
+
};
|
|
558
|
+
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
559
|
+
async function commandRows(commands, colors) {
|
|
560
|
+
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
561
|
+
const groups = new Map([["Commands:", []]]);
|
|
562
|
+
const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
|
|
563
|
+
for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
|
|
564
|
+
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
565
|
+
const rows = groups.get(heading) ?? [];
|
|
566
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
567
|
+
groups.set(heading, rows);
|
|
460
568
|
}
|
|
461
|
-
return
|
|
569
|
+
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
570
|
+
}
|
|
571
|
+
function takeOutputFlags(argv) {
|
|
572
|
+
let index = 0;
|
|
573
|
+
for (;index < argv.length; index++) {
|
|
574
|
+
const word = argv[index];
|
|
575
|
+
if (word === "--plain")
|
|
576
|
+
applyOutput(undefined, true);
|
|
577
|
+
else if (word.startsWith("--color="))
|
|
578
|
+
applyOutput(word.slice("--color=".length), undefined);
|
|
579
|
+
else if (word === "--color" && argv[index + 1] !== undefined)
|
|
580
|
+
applyOutput(argv[++index], undefined);
|
|
581
|
+
else
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
return argv.slice(index);
|
|
462
585
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return started;
|
|
469
|
-
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
470
|
-
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
586
|
+
async function resolveSummary(summary) {
|
|
587
|
+
return typeof summary === "string" ? summary : summary();
|
|
588
|
+
}
|
|
589
|
+
function scopeOf(commands, extra, aliases) {
|
|
590
|
+
return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
|
|
471
591
|
}
|
|
472
|
-
async function
|
|
592
|
+
async function expand(scope, words) {
|
|
593
|
+
if (scope.lazy.length === 0)
|
|
594
|
+
return scope;
|
|
595
|
+
const commands = { ...scope.commands };
|
|
596
|
+
for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
|
|
597
|
+
for (const [name, command] of Object.entries(added))
|
|
598
|
+
commands[name] ??= command;
|
|
599
|
+
}
|
|
600
|
+
return { ...scope, commands, lazy: [] };
|
|
601
|
+
}
|
|
602
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
473
603
|
const colors = paint(stream);
|
|
474
|
-
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
475
604
|
const prefix = path.join(" ");
|
|
605
|
+
const summary = await resolveSummary(lazySummary);
|
|
606
|
+
if (wantsHelpJson())
|
|
607
|
+
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
476
608
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
477
609
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
610
|
+
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
478
611
|
return [
|
|
479
|
-
header(colors),
|
|
612
|
+
header(colors, summary),
|
|
480
613
|
"",
|
|
481
614
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
615
|
+
...replacesRows(replaces, colors),
|
|
616
|
+
"",
|
|
617
|
+
...await commandRows(commands, colors),
|
|
618
|
+
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
482
619
|
"",
|
|
483
|
-
colors.bold("
|
|
484
|
-
...
|
|
485
|
-
...
|
|
620
|
+
colors.bold("Options:"),
|
|
621
|
+
...optionRows(path.length === 1 ? TOP_OPTIONS : LEVEL_OPTIONS, colors),
|
|
622
|
+
...exampleRows(examples ?? [], colors),
|
|
486
623
|
"",
|
|
487
624
|
`${colors.dim("Run")} ${prefix} ${colors.cyan("<command>")} --help ${colors.dim("for its options.")}`
|
|
488
625
|
].join(`
|
|
@@ -496,24 +633,38 @@ function printError(prefix, message, hint, cause) {
|
|
|
496
633
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
497
634
|
console.error(`${head} ${message}${under("cause", cause, colors.dim)}${under("hint", hint, colors.yellow)}`);
|
|
498
635
|
}
|
|
499
|
-
function
|
|
636
|
+
function printJsonError(message, hint, cause) {
|
|
637
|
+
console.error(JSON.stringify({ error: message, ...hint === undefined ? {} : { hint }, ...cause === undefined ? {} : { cause } }));
|
|
638
|
+
}
|
|
639
|
+
function wantsJson(argv) {
|
|
640
|
+
const end = argv.indexOf("--");
|
|
641
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
642
|
+
}
|
|
643
|
+
function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
500
644
|
if (error instanceof HelpShown)
|
|
501
645
|
return 0;
|
|
502
646
|
const help = error instanceof UsageError ? `run ${prefix} --help for its options` : undefined;
|
|
503
647
|
const named = error instanceof Error && !(error instanceof Exit) && error.name !== "Error" ? `${error.name}: ${error.message}` : undefined;
|
|
504
648
|
const cause = error instanceof Error && error.cause instanceof Error ? redact(error.cause.message) : undefined;
|
|
505
|
-
|
|
649
|
+
const report = json ? printJsonError : (message, hint, causeText) => printError(prefix, message, hint, causeText);
|
|
650
|
+
report(redact(named ?? (error instanceof Error ? error.message : String(error))), error instanceof Exit ? error.hint ?? help : undefined, cause);
|
|
506
651
|
return error instanceof Exit ? error.code : 1;
|
|
507
652
|
}
|
|
508
|
-
async function dispatch(level,
|
|
509
|
-
const [name, ...args] = argv;
|
|
653
|
+
async function dispatch(level, typed, { version, redact }) {
|
|
510
654
|
const prefix = level.path.join(" ");
|
|
655
|
+
let argv;
|
|
656
|
+
try {
|
|
657
|
+
argv = takeOutputFlags(typed);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return reportThrown(prefix, error);
|
|
660
|
+
}
|
|
661
|
+
const [name, ...args] = argv;
|
|
511
662
|
if (name === undefined && level.path.length > 1) {
|
|
512
|
-
console.error(await help(level, process5.stderr));
|
|
663
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stderr));
|
|
513
664
|
return 2;
|
|
514
665
|
}
|
|
515
666
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
516
|
-
console.log(await help(level, process5.stdout));
|
|
667
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stdout));
|
|
517
668
|
return 0;
|
|
518
669
|
}
|
|
519
670
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -522,6 +673,10 @@ async function dispatch(level, argv, { version, redact }) {
|
|
|
522
673
|
}
|
|
523
674
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
524
675
|
let rest = args;
|
|
676
|
+
if (command === undefined && level.lazy.length > 0) {
|
|
677
|
+
level = await expand(level, [...level.path.slice(1), ...argv]);
|
|
678
|
+
command = level.commands[level.aliases?.[name] ?? name];
|
|
679
|
+
}
|
|
525
680
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
526
681
|
command = level.commands[level.defaultCommand];
|
|
527
682
|
rest = argv;
|
|
@@ -533,12 +688,12 @@ async function dispatch(level, argv, { version, redact }) {
|
|
|
533
688
|
}
|
|
534
689
|
if ("commands" in command) {
|
|
535
690
|
const { summary } = command;
|
|
536
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
691
|
+
return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
537
692
|
}
|
|
538
693
|
try {
|
|
539
694
|
return await command.run(rest);
|
|
540
695
|
} catch (error) {
|
|
541
|
-
return reportThrown(`${prefix} ${name}`, error, redact);
|
|
696
|
+
return reportThrown(`${prefix} ${name}`, error, redact, wantsJson(rest));
|
|
542
697
|
}
|
|
543
698
|
}
|
|
544
699
|
var COMPLETE = "__complete";
|
|
@@ -558,18 +713,24 @@ async function optionsOf(run) {
|
|
|
558
713
|
async function leafCandidates(run, words) {
|
|
559
714
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
560
715
|
return [FILES];
|
|
561
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
716
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
562
717
|
}
|
|
563
|
-
async function candidates(
|
|
718
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
564
719
|
const [word = "", ...rest] = words;
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
720
|
+
const listing = rest.length === 0 && !word.startsWith("-");
|
|
721
|
+
let { commands, aliases } = scope;
|
|
722
|
+
let command = commands[aliases?.[word] ?? word];
|
|
723
|
+
if (listing || command === undefined) {
|
|
724
|
+
({ commands, aliases } = await expand(scope, [...before, ...words]));
|
|
725
|
+
command = commands[aliases?.[word] ?? word];
|
|
726
|
+
}
|
|
727
|
+
if (listing)
|
|
728
|
+
return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
|
|
568
729
|
if (command === undefined) {
|
|
569
730
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
570
731
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
571
732
|
}
|
|
572
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
733
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
573
734
|
}
|
|
574
735
|
async function printCandidates(found) {
|
|
575
736
|
for (const line of await found)
|
|
@@ -578,8 +739,8 @@ async function printCandidates(found) {
|
|
|
578
739
|
}
|
|
579
740
|
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
580
741
|
if (argv[0] === COMPLETE)
|
|
581
|
-
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
582
|
-
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${
|
|
742
|
+
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
743
|
+
return dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${about}`, summary: cli.about, ...scopeOf(cli.commands, cli.extraCommands, cli.aliases), defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
583
744
|
}
|
|
584
745
|
async function runMain({ name, version, run, redact }, argv = process5.argv.slice(2)) {
|
|
585
746
|
if (argv[0] === COMPLETE)
|
|
@@ -591,14 +752,15 @@ async function runMain({ name, version, run, redact }, argv = process5.argv.slic
|
|
|
591
752
|
try {
|
|
592
753
|
return await run(argv);
|
|
593
754
|
} catch (error) {
|
|
594
|
-
return reportThrown(name, error, redact);
|
|
755
|
+
return reportThrown(name, error, redact, wantsJson(argv));
|
|
595
756
|
}
|
|
596
757
|
}
|
|
597
758
|
// package.json
|
|
598
|
-
var version = "0.
|
|
759
|
+
var version = "0.6.0";
|
|
599
760
|
|
|
600
761
|
// src/files.ts
|
|
601
|
-
import { readdir } from "fs/promises";
|
|
762
|
+
import { chmod, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
763
|
+
import { basename, dirname, join } from "path";
|
|
602
764
|
|
|
603
765
|
// src/dotenv.ts
|
|
604
766
|
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
@@ -721,6 +883,17 @@ var TOKEN_FORMATS = [
|
|
|
721
883
|
["Slack token", /\bxox[abprs]-[\w-]{10,}/g],
|
|
722
884
|
["JWT", /\beyJ[\w-]+\.[\w-]+\.[\w-]+/g]
|
|
723
885
|
];
|
|
886
|
+
var URL_PARTS = /\b[a-z][a-z0-9+.-]*:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
887
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
888
|
+
var QUERY_PARAM = /([^&=]+)=([^&]*)/g;
|
|
889
|
+
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
890
|
+
var WEBHOOK_FORMATS = [
|
|
891
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
892
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
893
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
894
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
895
|
+
];
|
|
896
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
724
897
|
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
725
898
|
var PEM_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
726
899
|
function isSecretKey(key) {
|
|
@@ -744,6 +917,39 @@ function knownSecrets(pairs) {
|
|
|
744
917
|
function urlPasswords(text) {
|
|
745
918
|
return [...text.matchAll(URL_PASSWORD)].flatMap((url) => url[1] ?? []);
|
|
746
919
|
}
|
|
920
|
+
function urlTokens(line) {
|
|
921
|
+
const found = [];
|
|
922
|
+
const add = (kind, start, value) => {
|
|
923
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
924
|
+
};
|
|
925
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
926
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
927
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
928
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
929
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
930
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
931
|
+
continue;
|
|
932
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
933
|
+
const target = authority.slice(hostAt) + path;
|
|
934
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
935
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
936
|
+
if (start !== undefined && end !== undefined)
|
|
937
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
938
|
+
}
|
|
939
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
940
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
941
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
942
|
+
}
|
|
943
|
+
if (query === undefined || queryStart === undefined)
|
|
944
|
+
continue;
|
|
945
|
+
for (const param of query.matchAll(QUERY_PARAM)) {
|
|
946
|
+
const [name, value] = [param[1] ?? "", param[2] ?? ""];
|
|
947
|
+
if (value !== "" && SECRET_PARAM.test(name))
|
|
948
|
+
add("URL token", queryStart + param.index + name.length + 1, value);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return found.filter((match) => match.end > match.start);
|
|
952
|
+
}
|
|
747
953
|
function findSecrets(line, known) {
|
|
748
954
|
const found = [];
|
|
749
955
|
for (const url of line.matchAll(URL_PASSWORD)) {
|
|
@@ -755,6 +961,7 @@ function findSecrets(line, known) {
|
|
|
755
961
|
for (const token of line.matchAll(pattern))
|
|
756
962
|
found.push({ start: token.index, end: token.index + token[0].length, kind, mask: hint(token[0]) });
|
|
757
963
|
}
|
|
964
|
+
found.push(...urlTokens(line));
|
|
758
965
|
if (known?.pattern !== undefined) {
|
|
759
966
|
for (const value of line.matchAll(known.pattern))
|
|
760
967
|
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
@@ -796,7 +1003,20 @@ async function readEnv(path) {
|
|
|
796
1003
|
return parseDotenv(await file.text());
|
|
797
1004
|
}
|
|
798
1005
|
async function writeEnv(path, file) {
|
|
799
|
-
|
|
1006
|
+
let target = path;
|
|
1007
|
+
let mode = 384;
|
|
1008
|
+
try {
|
|
1009
|
+
target = await realpath(path);
|
|
1010
|
+
mode = (await stat(target)).mode & 511;
|
|
1011
|
+
} catch {}
|
|
1012
|
+
const temp = join(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1013
|
+
try {
|
|
1014
|
+
await writeFile(temp, serializeDotenv(file), { mode: 384, flag: "wx" });
|
|
1015
|
+
await chmod(temp, mode);
|
|
1016
|
+
await rename(temp, target);
|
|
1017
|
+
} finally {
|
|
1018
|
+
await rm(temp, { force: true });
|
|
1019
|
+
}
|
|
800
1020
|
}
|
|
801
1021
|
async function loadKnown(folder = process.cwd()) {
|
|
802
1022
|
const pairs = [];
|
|
@@ -911,14 +1131,16 @@ function check(text, known) {
|
|
|
911
1131
|
// src/redact.ts
|
|
912
1132
|
var usage = `redact [--check]
|
|
913
1133
|
|
|
914
|
-
Copies stdin to stdout line by line with secrets masked: known token formats (ghp_, github_pat_, glpat-, sk-, xox?-, JWTs), private key blocks, the password of any URL and the values of secret-looking keys in this folder's .env* files.
|
|
915
|
-
--check prints nothing of the input and lists the file and line of each secret found, exiting 1 when there is any: \`git diff --cached | redact --check
|
|
1134
|
+
Copies stdin to stdout line by line with secrets masked: known token formats (ghp_, github_pat_, glpat-, sk-, xox?-, JWTs), private key blocks, the password and the tokens of any URL and the values of secret-looking keys in this folder's .env* files.
|
|
1135
|
+
--check prints nothing of the input and lists the file and line of each secret found, exiting 1 when there is any: \`git diff --cached | redact --check\`.
|
|
1136
|
+
|
|
1137
|
+
Exits 0 when it copied the input or found no secret, 1 when --check finds one, 2 on a usage error.`;
|
|
916
1138
|
var options = {
|
|
917
1139
|
check: { type: "boolean", description: "list where secrets are instead of masking them, exit 1 when found" },
|
|
918
1140
|
"no-env": { type: "boolean", description: "do not search for the values of the .env* files" }
|
|
919
1141
|
};
|
|
920
1142
|
async function run(args) {
|
|
921
|
-
const parsed = parse(args, usage, options);
|
|
1143
|
+
const parsed = parse(args, usage, options, { replaces: "printing a command's secrets into a log or a transcript", examples: [["docker compose config | redact", "the resolved compose file, secrets masked"], ["git diff --cached | redact --check", "refuse a commit carrying a secret"]] });
|
|
922
1144
|
const known = parsed.values["no-env"] === true ? undefined : await loadKnown();
|
|
923
1145
|
if (parsed.values.check === true) {
|
|
924
1146
|
const findings = check(await readInput("-"), known);
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/env-tools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "ev lists and edits .env files with secrets masked, redact masks secrets in any piped output.",
|
|
6
|
-
"author": "Kaynooo <kaynooo
|
|
6
|
+
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://git.
|
|
8
|
+
"homepage": "https://git.kaynooo.fr/kaynooo/agent-tools/src/branch/main/packages/env#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://git.
|
|
11
|
+
"url": "git+https://git.kaynooo.fr/kaynooo/agent-tools.git",
|
|
12
12
|
"directory": "packages/env"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://git.
|
|
14
|
+
"bugs": "https://git.kaynooo.fr/kaynooo/agent-tools/issues",
|
|
15
15
|
"keywords": ["env", "dotenv", "secrets", "redact", "mask"],
|
|
16
16
|
"exports": {
|
|
17
17
|
"./dotenv": "./src/dotenv.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"bun": ">=1.4.3"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
|
-
"registry": "https://git.
|
|
29
|
+
"registry": "https://git.kaynooo.fr/api/packages/kaynooo/npm/"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "$npm_execpath build src/ev.ts src/redact.ts --target bun --outdir dist",
|