@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/ev.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);
|
|
568
|
+
}
|
|
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;
|
|
460
583
|
}
|
|
461
|
-
return
|
|
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,24 +752,25 @@ 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/commands/audit.ts
|
|
601
|
-
import { stat as
|
|
762
|
+
import { stat as stat4 } from "fs/promises";
|
|
602
763
|
import { relative, resolve } from "path";
|
|
603
764
|
import process7 from "process";
|
|
604
765
|
|
|
605
766
|
// src/audit.ts
|
|
606
|
-
import { readdir as readdir2, stat } from "fs/promises";
|
|
607
|
-
import { dirname, join } from "path";
|
|
767
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
768
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
608
769
|
var {$ } = globalThis.Bun;
|
|
609
770
|
|
|
610
771
|
// src/files.ts
|
|
611
|
-
import { readdir } from "fs/promises";
|
|
772
|
+
import { chmod, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
773
|
+
import { basename, dirname, join } from "path";
|
|
612
774
|
|
|
613
775
|
// src/dotenv.ts
|
|
614
776
|
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
@@ -731,6 +893,17 @@ var TOKEN_FORMATS = [
|
|
|
731
893
|
["Slack token", /\bxox[abprs]-[\w-]{10,}/g],
|
|
732
894
|
["JWT", /\beyJ[\w-]+\.[\w-]+\.[\w-]+/g]
|
|
733
895
|
];
|
|
896
|
+
var URL_PARTS = /\b[a-z][a-z0-9+.-]*:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
897
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
898
|
+
var QUERY_PARAM = /([^&=]+)=([^&]*)/g;
|
|
899
|
+
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
900
|
+
var WEBHOOK_FORMATS = [
|
|
901
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
902
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
903
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
904
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
905
|
+
];
|
|
906
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
734
907
|
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
735
908
|
var PEM_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
736
909
|
function isSecretKey(key) {
|
|
@@ -754,6 +927,39 @@ function knownSecrets(pairs) {
|
|
|
754
927
|
function urlPasswords(text) {
|
|
755
928
|
return [...text.matchAll(URL_PASSWORD)].flatMap((url) => url[1] ?? []);
|
|
756
929
|
}
|
|
930
|
+
function urlTokens(line) {
|
|
931
|
+
const found = [];
|
|
932
|
+
const add = (kind, start, value) => {
|
|
933
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
934
|
+
};
|
|
935
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
936
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
937
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
938
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
939
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
940
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
941
|
+
continue;
|
|
942
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
943
|
+
const target = authority.slice(hostAt) + path;
|
|
944
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
945
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
946
|
+
if (start !== undefined && end !== undefined)
|
|
947
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
948
|
+
}
|
|
949
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
950
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
951
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
952
|
+
}
|
|
953
|
+
if (query === undefined || queryStart === undefined)
|
|
954
|
+
continue;
|
|
955
|
+
for (const param of query.matchAll(QUERY_PARAM)) {
|
|
956
|
+
const [name, value] = [param[1] ?? "", param[2] ?? ""];
|
|
957
|
+
if (value !== "" && SECRET_PARAM.test(name))
|
|
958
|
+
add("URL token", queryStart + param.index + name.length + 1, value);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return found.filter((match) => match.end > match.start);
|
|
962
|
+
}
|
|
757
963
|
function findSecrets(line, known) {
|
|
758
964
|
const found = [];
|
|
759
965
|
for (const url of line.matchAll(URL_PASSWORD)) {
|
|
@@ -765,6 +971,7 @@ function findSecrets(line, known) {
|
|
|
765
971
|
for (const token of line.matchAll(pattern))
|
|
766
972
|
found.push({ start: token.index, end: token.index + token[0].length, kind, mask: hint(token[0]) });
|
|
767
973
|
}
|
|
974
|
+
found.push(...urlTokens(line));
|
|
768
975
|
if (known?.pattern !== undefined) {
|
|
769
976
|
for (const value of line.matchAll(known.pattern))
|
|
770
977
|
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
@@ -806,7 +1013,20 @@ async function readEnv(path) {
|
|
|
806
1013
|
return parseDotenv(await file.text());
|
|
807
1014
|
}
|
|
808
1015
|
async function writeEnv(path, file) {
|
|
809
|
-
|
|
1016
|
+
let target = path;
|
|
1017
|
+
let mode = 384;
|
|
1018
|
+
try {
|
|
1019
|
+
target = await realpath(path);
|
|
1020
|
+
mode = (await stat(target)).mode & 511;
|
|
1021
|
+
} catch {}
|
|
1022
|
+
const temp = join(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1023
|
+
try {
|
|
1024
|
+
await writeFile(temp, serializeDotenv(file), { mode: 384, flag: "wx" });
|
|
1025
|
+
await chmod(temp, mode);
|
|
1026
|
+
await rename(temp, target);
|
|
1027
|
+
} finally {
|
|
1028
|
+
await rm(temp, { force: true });
|
|
1029
|
+
}
|
|
810
1030
|
}
|
|
811
1031
|
async function loadKnown(folder = process.cwd()) {
|
|
812
1032
|
const pairs = [];
|
|
@@ -842,16 +1062,16 @@ function inlineSecrets(compose) {
|
|
|
842
1062
|
return [...new Set(names)];
|
|
843
1063
|
}
|
|
844
1064
|
async function openToOthers(path) {
|
|
845
|
-
if (((await
|
|
1065
|
+
if (((await stat2(path)).mode & 4) === 0)
|
|
846
1066
|
return false;
|
|
847
|
-
for (let dir =
|
|
848
|
-
if (((await
|
|
1067
|
+
for (let dir = dirname2(path);dir !== "/"; dir = dirname2(dir)) {
|
|
1068
|
+
if (((await stat2(dir)).mode & 1) === 0)
|
|
849
1069
|
return false;
|
|
850
1070
|
}
|
|
851
1071
|
return true;
|
|
852
1072
|
}
|
|
853
1073
|
async function tracked(path) {
|
|
854
|
-
return (await $`git ls-files --error-unmatch -- ${path}`.cwd(
|
|
1074
|
+
return (await $`git ls-files --error-unmatch -- ${path}`.cwd(dirname2(path)).quiet().nothrow()).exitCode === 0;
|
|
855
1075
|
}
|
|
856
1076
|
async function* candidates2(root, unread, depth = 0) {
|
|
857
1077
|
let found;
|
|
@@ -862,7 +1082,7 @@ async function* candidates2(root, unread, depth = 0) {
|
|
|
862
1082
|
return;
|
|
863
1083
|
}
|
|
864
1084
|
for (const entry of found) {
|
|
865
|
-
const path =
|
|
1085
|
+
const path = join2(root, entry.name);
|
|
866
1086
|
if (entry.isDirectory() && depth < MAX_DEPTH && !SKIPPED.has(entry.name))
|
|
867
1087
|
yield* candidates2(path, unread, depth + 1);
|
|
868
1088
|
else if (entry.isFile() && !TEMPLATE.test(entry.name) && (ENV_FILE.test(entry.name) || COMPOSE_FILE.test(entry.name)))
|
|
@@ -891,8 +1111,8 @@ async function audit(roots) {
|
|
|
891
1111
|
}
|
|
892
1112
|
|
|
893
1113
|
// src/fix.ts
|
|
894
|
-
import { chmod, chown, rm, stat as
|
|
895
|
-
import { dirname as
|
|
1114
|
+
import { chmod as chmod2, chown, rm as rm2, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
1115
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
896
1116
|
var MAP_ENTRY = /^(\s*)(["']?)([A-Z0-9_]+)\2(\s*:)(.*)$/;
|
|
897
1117
|
var LIST_ENTRY = /^(\s*-[ \t]*)(["']?)([A-Z0-9_]+)=(.*)$/;
|
|
898
1118
|
var TRAILING = /^\s*(?:#.*)?$/;
|
|
@@ -954,7 +1174,7 @@ function envLine(key, value) {
|
|
|
954
1174
|
`) ? undefined : `${key}='${value}'`;
|
|
955
1175
|
}
|
|
956
1176
|
async function resolved(path) {
|
|
957
|
-
const run = Bun.spawn(["docker", "compose", "-f", path, "config", "--format", "json"], { cwd:
|
|
1177
|
+
const run = Bun.spawn(["docker", "compose", "-f", path, "config", "--format", "json"], { cwd: dirname3(path), stdout: "pipe", stderr: "ignore", stdin: "ignore" });
|
|
958
1178
|
const [out, code] = await Promise.all([new Response(run.stdout).text(), run.exited]);
|
|
959
1179
|
return code === 0 ? out : undefined;
|
|
960
1180
|
}
|
|
@@ -965,7 +1185,7 @@ function gainedBy(before, after, key) {
|
|
|
965
1185
|
}
|
|
966
1186
|
async function shutOthersOut(path) {
|
|
967
1187
|
try {
|
|
968
|
-
await
|
|
1188
|
+
await chmod2(path, (await stat3(path)).mode & 504);
|
|
969
1189
|
return true;
|
|
970
1190
|
} catch (error) {
|
|
971
1191
|
if (error.code === "EPERM")
|
|
@@ -974,7 +1194,7 @@ async function shutOthersOut(path) {
|
|
|
974
1194
|
}
|
|
975
1195
|
}
|
|
976
1196
|
async function fixCompose(path) {
|
|
977
|
-
const env =
|
|
1197
|
+
const env = join3(dirname3(path), ".env");
|
|
978
1198
|
const compose = await Bun.file(path).text();
|
|
979
1199
|
const envBefore = await Bun.file(env).exists() ? await Bun.file(env).text() : undefined;
|
|
980
1200
|
const dotenv = parseDotenv(envBefore ?? "");
|
|
@@ -1026,15 +1246,15 @@ async function move(fix, compose, composeAfter, envBefore, envAfter) {
|
|
|
1026
1246
|
const docker = Bun.which("docker") !== null;
|
|
1027
1247
|
const before = docker ? await resolved(path) : undefined;
|
|
1028
1248
|
if (envBefore === undefined) {
|
|
1029
|
-
await
|
|
1030
|
-
const { uid, gid } = await
|
|
1249
|
+
await writeFile2(env, envAfter, { mode: 384 });
|
|
1250
|
+
const { uid, gid } = await stat3(path);
|
|
1031
1251
|
try {
|
|
1032
1252
|
await chown(env, uid, gid);
|
|
1033
1253
|
} catch {}
|
|
1034
1254
|
} else {
|
|
1035
|
-
await
|
|
1255
|
+
await writeFile2(env, envAfter);
|
|
1036
1256
|
}
|
|
1037
|
-
await
|
|
1257
|
+
await writeFile2(path, composeAfter);
|
|
1038
1258
|
if (!docker) {
|
|
1039
1259
|
fix.check = "no docker";
|
|
1040
1260
|
return;
|
|
@@ -1048,11 +1268,11 @@ async function move(fix, compose, composeAfter, envBefore, envAfter) {
|
|
|
1048
1268
|
fix.check = "same";
|
|
1049
1269
|
return;
|
|
1050
1270
|
}
|
|
1051
|
-
await
|
|
1271
|
+
await writeFile2(path, compose);
|
|
1052
1272
|
if (envBefore === undefined)
|
|
1053
|
-
await
|
|
1273
|
+
await rm2(env);
|
|
1054
1274
|
else
|
|
1055
|
-
await
|
|
1275
|
+
await writeFile2(env, envBefore);
|
|
1056
1276
|
fix.check = "rolled back";
|
|
1057
1277
|
fix.left.push(...fix.moved.map((key) => {
|
|
1058
1278
|
const gained = after === undefined ? [] : gainedBy(before, after, key);
|
|
@@ -1067,7 +1287,7 @@ var usage = `ev audit [dir...] [--fix]
|
|
|
1067
1287
|
Lists the .env files any account on the machine can read or git tracks, and the compose files any account can read that write a secret out (a secret-named value or a URL password) instead of reading it from a .env, never printing a value.
|
|
1068
1288
|
Looks under each folder (the current one by default) down to 5 levels, skipping node_modules, vendor and .git, and leaves .example, .sample, .template and .dist files out. A folder it cannot read is named on stderr.
|
|
1069
1289
|
With --fix, moves each secret a listed compose file writes out into the .env beside it as \${KEY}, puts it back when \`docker compose config\` resolves differently after, and shuts other accounts out of both files and of every listed .env; a key the .env already sets to another value stays put.
|
|
1070
|
-
Exits
|
|
1290
|
+
Exits 0 when it finds none (with --fix, when none is left), 1 when it finds one, 2 on a usage error.`;
|
|
1071
1291
|
var options = {
|
|
1072
1292
|
json: jsonOption("the findings and the folders it could not read"),
|
|
1073
1293
|
fix: { type: "boolean", description: "move inline compose secrets into a .env and shut other accounts out" }
|
|
@@ -1082,7 +1302,7 @@ var CHECKS = {
|
|
|
1082
1302
|
};
|
|
1083
1303
|
async function isFolder(path) {
|
|
1084
1304
|
try {
|
|
1085
|
-
return (await
|
|
1305
|
+
return (await stat4(path)).isDirectory();
|
|
1086
1306
|
} catch {
|
|
1087
1307
|
return false;
|
|
1088
1308
|
}
|
|
@@ -1149,7 +1369,7 @@ async function audit2(args) {
|
|
|
1149
1369
|
}
|
|
1150
1370
|
const uid = process7.getuid?.();
|
|
1151
1371
|
for (const { path, readable, tracked, secrets } of exposed) {
|
|
1152
|
-
const sudo = uid !== undefined && uid !== 0 && (await
|
|
1372
|
+
const sudo = uid !== undefined && uid !== 0 && (await stat4(path)).uid !== uid ? "sudo " : "";
|
|
1153
1373
|
const problems = [
|
|
1154
1374
|
...secrets === undefined ? [] : [`writes out ${secrets.join(", ")}: ev audit --fix moves the values into a .env beside it`],
|
|
1155
1375
|
...readable ? [`readable by any account: ${sudo}chmod o-rwx ${quoted2(shown(path))}`] : [],
|
|
@@ -1165,10 +1385,14 @@ async function audit2(args) {
|
|
|
1165
1385
|
// src/commands/compare.ts
|
|
1166
1386
|
var diffUsage = `ev diff <a> <b>
|
|
1167
1387
|
|
|
1168
|
-
Names the keys set in one file and not the other, never their values.
|
|
1388
|
+
Names the keys set in one file and not the other, never their values.
|
|
1389
|
+
|
|
1390
|
+
Exits 0 when both set the same keys, 1 when they differ, 2 on a usage error.`;
|
|
1169
1391
|
var missingUsage = `ev missing [file]
|
|
1170
1392
|
|
|
1171
|
-
Names the keys the reference file sets that the file (.env by default) does not.
|
|
1393
|
+
Names the keys the reference file sets that the file (.env by default) does not.
|
|
1394
|
+
|
|
1395
|
+
Exits 0 when none is missing, 1 when some are, 2 on a usage error.`;
|
|
1172
1396
|
async function keys(path) {
|
|
1173
1397
|
return new Set(entries(await readEnv(path)).keys());
|
|
1174
1398
|
}
|
|
@@ -1209,13 +1433,24 @@ var DEFAULT_FILE = ".env";
|
|
|
1209
1433
|
var TRAILING_NEWLINE = /\r?\n$/;
|
|
1210
1434
|
var setUsage = `ev set KEY [file]
|
|
1211
1435
|
|
|
1212
|
-
Sets KEY in the file (.env by default, created when missing), replacing its line in place or appending one. The value is read from stdin, or from -F <file>, so it never appears in a command line; --empty sets it to an empty value
|
|
1436
|
+
Sets KEY in the file (.env by default, created when missing), replacing its line in place or appending one. The value is read from stdin, or from -F <file>, so it never appears in a command line; --empty sets it to an empty value.
|
|
1437
|
+
|
|
1438
|
+
Exits 0 when it wrote the key, 1 when it could not, 2 on a usage error.`;
|
|
1213
1439
|
var unsetUsage = `ev unset KEY [file]
|
|
1214
1440
|
|
|
1215
|
-
Removes every line setting KEY from the file (.env by default), leaving comments and other lines as they are
|
|
1441
|
+
Removes every line setting KEY from the file (.env by default), leaving comments and other lines as they are.
|
|
1442
|
+
|
|
1443
|
+
Exits 0 when it removed the key, 1 when the file does not set it, 2 on a usage error.`;
|
|
1216
1444
|
var mvUsage = `ev mv KEY --from <file> --to <file>
|
|
1217
1445
|
|
|
1218
|
-
Moves KEY's line from one file to another, as written. Refuses to replace a key the target already sets unless --force
|
|
1446
|
+
Moves KEY's line from one file to another, as written. Refuses to replace a key the target already sets unless --force.
|
|
1447
|
+
|
|
1448
|
+
Exits 0 when it moved the key, 1 when the source lacks it or the target already sets it, 2 on a usage error.`;
|
|
1449
|
+
var cpUsage = `ev cp KEY --from <file> --to <file> [--as NEWKEY]
|
|
1450
|
+
|
|
1451
|
+
Copies KEY's value from one file to another without printing it, replacing the key's line in the target in place or appending one, and prints the masked result as ev set does. --as stores the copy under another key.
|
|
1452
|
+
|
|
1453
|
+
Exits 0 when it copied the key, 1 when the source lacks it, 2 on a usage error.`;
|
|
1219
1454
|
async function readValue(from) {
|
|
1220
1455
|
const input = await readInput(from ?? "-", { noStdin: "pipe the value on stdin or give -F <file>, it is never taken from the command line" });
|
|
1221
1456
|
if (input === "")
|
|
@@ -1259,6 +1494,22 @@ ${unsetUsage}`);
|
|
|
1259
1494
|
console.log(`${target}: removed ${key}`);
|
|
1260
1495
|
return 0;
|
|
1261
1496
|
}
|
|
1497
|
+
async function transfer(key, from, to, newKey, replace) {
|
|
1498
|
+
if (from === to && newKey === undefined)
|
|
1499
|
+
fail("--from and --to are the same file");
|
|
1500
|
+
const source = await readEnv(from);
|
|
1501
|
+
const line = lastLine(source, key);
|
|
1502
|
+
if (line === undefined)
|
|
1503
|
+
fail(`${key} is not set in ${from}`, 1);
|
|
1504
|
+
const target = from === to ? source : await readOrEmpty(to);
|
|
1505
|
+
const name = newKey ?? key;
|
|
1506
|
+
if (!replace && entries(target).has(name))
|
|
1507
|
+
fail(`${to} already sets ${name}, --force replaces it`, 1);
|
|
1508
|
+
const value = line.value ?? "";
|
|
1509
|
+
setLine(target, name, value, newKey === undefined && !entries(target).has(name) ? line.raw : undefined);
|
|
1510
|
+
await writeEnv(to, target);
|
|
1511
|
+
return { source, value, name };
|
|
1512
|
+
}
|
|
1262
1513
|
async function mv(args) {
|
|
1263
1514
|
const parsed = parse(args, mvUsage, {
|
|
1264
1515
|
from: { type: "string", description: "the file to take KEY from" },
|
|
@@ -1271,31 +1522,42 @@ async function mv(args) {
|
|
|
1271
1522
|
fail(`needs KEY, --from and --to
|
|
1272
1523
|
|
|
1273
1524
|
${mvUsage}`);
|
|
1274
|
-
|
|
1275
|
-
fail("--from and --to are the same file");
|
|
1276
|
-
const source = await readEnv(from);
|
|
1277
|
-
const line = lastLine(source, key);
|
|
1278
|
-
if (line === undefined)
|
|
1279
|
-
fail(`${key} is not set in ${from}`, 1);
|
|
1280
|
-
const target = await readOrEmpty(to);
|
|
1281
|
-
if (entries(target).has(key) && force !== true)
|
|
1282
|
-
fail(`${to} already sets ${key}, --force replaces it`, 1);
|
|
1283
|
-
setLine(target, key, line.value ?? "", line.raw);
|
|
1284
|
-
await writeEnv(to, target);
|
|
1525
|
+
const { source } = await transfer(key, from, to, undefined, force === true);
|
|
1285
1526
|
removeKey(source, key);
|
|
1286
1527
|
await writeEnv(from, source);
|
|
1287
1528
|
console.log(`${key}: ${from} -> ${to}`);
|
|
1288
1529
|
return 0;
|
|
1289
1530
|
}
|
|
1531
|
+
async function cp(args) {
|
|
1532
|
+
const parsed = parse(args, cpUsage, {
|
|
1533
|
+
from: { type: "string", description: "the file to take KEY from" },
|
|
1534
|
+
to: { type: "string", description: "the file to put the copy in, created when missing" },
|
|
1535
|
+
as: { type: "string", description: "store the copy under this key instead" }
|
|
1536
|
+
});
|
|
1537
|
+
const [key] = parsed.positionals;
|
|
1538
|
+
const { from, to, as } = parsed.values;
|
|
1539
|
+
if (key === undefined || from === undefined || to === undefined)
|
|
1540
|
+
fail(`needs KEY, --from and --to
|
|
1541
|
+
|
|
1542
|
+
${cpUsage}`);
|
|
1543
|
+
const { value, name } = await transfer(key, from, to, as, true);
|
|
1544
|
+
console.log(`${to}: ${name}=${maskValue(name, value).replaceAll(`
|
|
1545
|
+
`, "\\n")}`);
|
|
1546
|
+
return 0;
|
|
1547
|
+
}
|
|
1290
1548
|
|
|
1291
1549
|
// src/commands/read.ts
|
|
1292
1550
|
var {$: $2 } = globalThis.Bun;
|
|
1293
1551
|
var listUsage = `ev list [file]
|
|
1294
1552
|
|
|
1295
|
-
Prints every key of the folder's .env* files, or of one file, with secret values masked: a few leading characters and the length under a key named like TOKEN, SECRET, PASSWORD, KEY, PRIVATE or DSN, and the password of any URL
|
|
1553
|
+
Prints every key of the folder's .env* files, or of one file, with secret values masked: a few leading characters and the length under a key named like TOKEN, SECRET, PASSWORD, KEY, PRIVATE or DSN, and the password and tokens of any URL.
|
|
1554
|
+
|
|
1555
|
+
Exits 0 when it printed them, 1 when there is no .env* file, 2 on a usage error.`;
|
|
1296
1556
|
var getUsage = `ev get KEY [file]
|
|
1297
1557
|
|
|
1298
|
-
Prints the masked value of one key, from every .env* file that sets it or from one file
|
|
1558
|
+
Prints the masked value of one key, from every .env* file that sets it or from one file.
|
|
1559
|
+
|
|
1560
|
+
Exits 0 when the key is set, 1 when it is not set anywhere, 2 on a usage error.`;
|
|
1299
1561
|
function display(key, value) {
|
|
1300
1562
|
return maskValue(key, value).replaceAll(`
|
|
1301
1563
|
`, "\\n").replaceAll("\r", "\\r");
|
|
@@ -1336,10 +1598,14 @@ ${getUsage}`);
|
|
|
1336
1598
|
}
|
|
1337
1599
|
var grepUsage = `ev grep [-i] <regex> [file...]
|
|
1338
1600
|
|
|
1339
|
-
Prints the keys matching the regular expression, from the folder's .env* files or the files given, with their values masked as ev list does. The regex is matched against the key only, never the value: \`ev grep '^DATABASE_'\` instead of \`grep ^DATABASE_ .env
|
|
1601
|
+
Prints the keys matching the regular expression, from the folder's .env* files or the files given, with their values masked as ev list does. The regex is matched against the key only, never the value: \`ev grep '^DATABASE_'\` instead of \`grep ^DATABASE_ .env\`.
|
|
1602
|
+
|
|
1603
|
+
Exits 0 when a key matches, 1 when none does, 2 on a usage error.`;
|
|
1340
1604
|
var showUsage = `ev show <rev>:<file>
|
|
1341
1605
|
|
|
1342
|
-
Prints every key of an env file as a git revision holds it, values masked as ev list does: \`ev show HEAD~1:api/.env\` instead of \`git show HEAD~1:api/.env
|
|
1606
|
+
Prints every key of an env file as a git revision holds it, values masked as ev list does: \`ev show HEAD~1:api/.env\` instead of \`git show HEAD~1:api/.env\`.
|
|
1607
|
+
|
|
1608
|
+
Exits 0 when it printed them, 1 when git cannot show the file, 2 on a usage error.`;
|
|
1343
1609
|
var REVISION = /^([^:]+):(.+)$/;
|
|
1344
1610
|
async function grep(args) {
|
|
1345
1611
|
const parsed = parse(args, grepUsage, { "ignore-case": { type: "boolean", short: "i", description: "match the key whatever its case" } });
|
|
@@ -1389,6 +1655,8 @@ process8.exitCode = await runCli({
|
|
|
1389
1655
|
name: "ev",
|
|
1390
1656
|
version,
|
|
1391
1657
|
about: ".env files listed and edited with secrets masked",
|
|
1658
|
+
replaces: "cat .env, grep on env files and editing them by hand",
|
|
1659
|
+
examples: ["ev list", ["ev get DATABASE_URL", "one key, its value masked"]],
|
|
1392
1660
|
commands: {
|
|
1393
1661
|
list: { run: list, summary: "every key of the .env* files, secrets masked" },
|
|
1394
1662
|
get: { run: get, summary: "one key's masked value" },
|
|
@@ -1396,6 +1664,7 @@ process8.exitCode = await runCli({
|
|
|
1396
1664
|
show: { run: show, summary: "an env file at a git revision, values masked" },
|
|
1397
1665
|
set: { run: set, summary: "set a key, the value read from stdin or -F" },
|
|
1398
1666
|
unset: { run: unset, summary: "remove a key from a file" },
|
|
1667
|
+
cp: { run: cp, summary: "copy a key from one file to another, the value never printed" },
|
|
1399
1668
|
mv: { run: mv, summary: "move a key from one file to another" },
|
|
1400
1669
|
diff: { run: diff, summary: "keys set in one file and not the other" },
|
|
1401
1670
|
missing: { run: missing, summary: "keys of .env.example that .env lacks" },
|