@kaynooo/cli-docker 0.4.0 → 0.5.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 +5 -3
- package/dist/dc-cli.js +442 -32
- package/dist/dlog-cli.js +349 -48
- package/dist/dx-cli.js +473 -47
- package/dist/sf-cli.js +488 -55
- package/package.json +1 -1
- package/src/dc-cli.ts +1 -2
- package/src/dlog-cli.ts +1 -2
- package/src/dlog.ts +61 -21
- package/src/dx-cli.ts +1 -2
- package/src/dx.ts +41 -10
- package/src/sf-cli.ts +1 -2
- package/src/sf.ts +41 -7
- package/src/stack.ts +24 -12
package/dist/dlog-cli.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
// src/dlog-cli.ts
|
|
5
5
|
import process9 from "process";
|
|
6
|
-
|
|
7
6
|
// ../kit/src/args.ts
|
|
8
7
|
import process4 from "process";
|
|
9
8
|
import { parseArgs } from "util";
|
|
@@ -306,17 +305,23 @@ var FULL_STOP = /\.$/;
|
|
|
306
305
|
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
307
306
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
308
307
|
}
|
|
309
|
-
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
|
|
310
|
-
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
308
|
+
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces, exits = [] } = {}) {
|
|
309
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exitRows(exits, colors), ...exampleRows(examples, colors)].join(`
|
|
311
310
|
`);
|
|
312
311
|
}
|
|
312
|
+
function exitRows(exits, colors = paint(process3.stdout)) {
|
|
313
|
+
if (exits.length === 0)
|
|
314
|
+
return [];
|
|
315
|
+
const width = Math.max(...exits.map(([code]) => String(code).length));
|
|
316
|
+
return ["", colors.bold("Exits:"), ...exits.map(([code, meaning]) => ` ${colors.cyan(String(code).padEnd(width))} ${meaning}`)];
|
|
317
|
+
}
|
|
313
318
|
function replacesRows(replaces, colors = paint(process3.stdout)) {
|
|
314
319
|
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
315
320
|
}
|
|
316
321
|
function wantsHelpJson(env = process3.env) {
|
|
317
322
|
return env.KIT_HELP === "json";
|
|
318
323
|
}
|
|
319
|
-
function description(usage, { examples = [], replaces } = {}) {
|
|
324
|
+
function description(usage, { examples = [], replaces, exits } = {}, options) {
|
|
320
325
|
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
321
326
|
`);
|
|
322
327
|
const first = lines[0];
|
|
@@ -327,7 +332,9 @@ function description(usage, { examples = [], replaces } = {}) {
|
|
|
327
332
|
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
328
333
|
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
329
334
|
...replaces === undefined ? {} : { replaces },
|
|
330
|
-
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
335
|
+
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] }),
|
|
336
|
+
...exits === undefined ? {} : { exits: exits.map(([code, meaning]) => ({ code, meaning })) },
|
|
337
|
+
...options === undefined ? {} : { options: Object.entries(options).map(([name, { short, description }]) => ({ name, ...short === undefined ? {} : { short }, ...description === undefined ? {} : { description } })) }
|
|
331
338
|
};
|
|
332
339
|
}
|
|
333
340
|
function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
@@ -369,6 +376,16 @@ function closest(name, names) {
|
|
|
369
376
|
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
370
377
|
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
371
378
|
}
|
|
379
|
+
// ../kit/src/input.ts
|
|
380
|
+
var WHOLE = /^\d+$/;
|
|
381
|
+
function wholeNumber(value, name, fallback, min = 1) {
|
|
382
|
+
if (value === undefined)
|
|
383
|
+
return fallback;
|
|
384
|
+
const number = Number(value);
|
|
385
|
+
if (!WHOLE.test(value) || !Number.isSafeInteger(number) || number < min)
|
|
386
|
+
badUsage(`${name.startsWith("<") ? name : `--${name}`} takes a whole number from ${min}, not ${value}`);
|
|
387
|
+
return number;
|
|
388
|
+
}
|
|
372
389
|
|
|
373
390
|
// ../kit/src/args.ts
|
|
374
391
|
var DASHES = /^-+/;
|
|
@@ -442,7 +459,7 @@ function parse(args, usage, options, settings = {}) {
|
|
|
442
459
|
const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
|
|
443
460
|
let parsed;
|
|
444
461
|
try {
|
|
445
|
-
parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
462
|
+
parsed = parseArgs({ args: joinDashValues(withBare(withAliases(own, all), all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
446
463
|
} catch (error) {
|
|
447
464
|
const message = parseError(error);
|
|
448
465
|
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
@@ -450,7 +467,7 @@ function parse(args, usage, options, settings = {}) {
|
|
|
450
467
|
const values = parsed.values;
|
|
451
468
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
452
469
|
if (values.help === true) {
|
|
453
|
-
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
|
|
470
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings, listing)) : helpText(usage, listing, undefined, settings));
|
|
454
471
|
throw new HelpShown;
|
|
455
472
|
}
|
|
456
473
|
checkChoices(parsed.values, all);
|
|
@@ -492,6 +509,18 @@ function listed(options, allowNegative) {
|
|
|
492
509
|
return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
|
|
493
510
|
}));
|
|
494
511
|
}
|
|
512
|
+
function withAliases(args, options) {
|
|
513
|
+
const named = new Map(Object.entries(options).flatMap(([name, { aliases = [] }]) => aliases.map((alias) => [`--${alias}`, `--${name}`])));
|
|
514
|
+
if (named.size === 0)
|
|
515
|
+
return args;
|
|
516
|
+
const separator = args.indexOf("--");
|
|
517
|
+
const own = separator === -1 ? args : args.slice(0, separator);
|
|
518
|
+
return [...own.map((arg) => {
|
|
519
|
+
const at = arg.indexOf("=");
|
|
520
|
+
const current = named.get(at === -1 ? arg : arg.slice(0, at));
|
|
521
|
+
return current === undefined ? arg : `${current}${at === -1 ? "" : arg.slice(at)}`;
|
|
522
|
+
}), ...separator === -1 ? [] : args.slice(separator)];
|
|
523
|
+
}
|
|
495
524
|
function withBare(args, options) {
|
|
496
525
|
const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
|
|
497
526
|
if (bare.size === 0)
|
|
@@ -582,6 +611,8 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
|
|
|
582
611
|
var RESET_STYLE = `${ESC}[0m`;
|
|
583
612
|
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
584
613
|
// ../kit/src/cli.ts
|
|
614
|
+
import { readFileSync, realpathSync } from "fs";
|
|
615
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
585
616
|
import process6 from "process";
|
|
586
617
|
|
|
587
618
|
// ../kit/src/usage.ts
|
|
@@ -724,11 +755,22 @@ async function printCandidates(found) {
|
|
|
724
755
|
console.log(line);
|
|
725
756
|
return 0;
|
|
726
757
|
}
|
|
758
|
+
function packageVersion(entry = Bun.main) {
|
|
759
|
+
for (let dir = dirname2(realpathSync(entry));; dir = dirname2(dir)) {
|
|
760
|
+
try {
|
|
761
|
+
const { version } = JSON.parse(readFileSync(join2(dir, "package.json"), "utf8"));
|
|
762
|
+
if (typeof version === "string")
|
|
763
|
+
return version;
|
|
764
|
+
} catch {}
|
|
765
|
+
if (dirname2(dir) === dir)
|
|
766
|
+
return "0.0.0";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
727
769
|
async function runMain({ name, version, run, redact, usage }, argv = process6.argv.slice(2)) {
|
|
728
770
|
if (argv[0] === COMPLETE)
|
|
729
771
|
return printCandidates(leafCandidates(run, argv.slice(1)));
|
|
730
772
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
731
|
-
console.log(version);
|
|
773
|
+
console.log(version ?? packageVersion());
|
|
732
774
|
return 0;
|
|
733
775
|
}
|
|
734
776
|
let code;
|
|
@@ -737,28 +779,124 @@ async function runMain({ name, version, run, redact, usage }, argv = process6.ar
|
|
|
737
779
|
} catch (error) {
|
|
738
780
|
code = reportThrown(name, error, redact, wantsJson(argv));
|
|
739
781
|
}
|
|
740
|
-
if (usage !== false)
|
|
741
|
-
|
|
782
|
+
if (usage !== false) {
|
|
783
|
+
try {
|
|
784
|
+
recordUsage(usageEntry(name, undefined, argv, code));
|
|
785
|
+
} catch {}
|
|
786
|
+
}
|
|
742
787
|
return code;
|
|
743
788
|
}
|
|
789
|
+
// ../kit/src/compose.ts
|
|
790
|
+
async function captureOutput(argv, { cwd, env } = {}) {
|
|
791
|
+
const run = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, env: { ...process.env, ...env }, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
792
|
+
const [stdout, stderr, code] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]);
|
|
793
|
+
return { code, stdout, stderr };
|
|
794
|
+
}
|
|
795
|
+
function composeError({ code, stderr }) {
|
|
796
|
+
const last = stderr.trim().split(`
|
|
797
|
+
`).at(-1) ?? "";
|
|
798
|
+
return last === "" ? `exit ${code}` : last;
|
|
799
|
+
}
|
|
744
800
|
// ../kit/src/secrets.ts
|
|
801
|
+
var SECRET_PART = /token|secret|passw|credential|private|dsn|apikey|accesskey|key$/i;
|
|
745
802
|
var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
|
|
803
|
+
var WORD_BREAKS = /[^a-zA-Z\d]+|(?<=[a-z\d])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/;
|
|
804
|
+
function isSecretName(name) {
|
|
805
|
+
return SECRET_PART.test(name) || name.split(WORD_BREAKS).some((word) => SECRET_WORDS.has(word.toLowerCase()));
|
|
806
|
+
}
|
|
746
807
|
// ../kit/src/sessions.ts
|
|
747
808
|
var {Glob } = globalThis.Bun;
|
|
748
809
|
var SESSION_FILES = new Glob("*.json");
|
|
749
810
|
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
750
|
-
// package.json
|
|
751
|
-
var version = "0.4.0";
|
|
752
|
-
|
|
753
811
|
// src/dlog.ts
|
|
754
812
|
import process8 from "process";
|
|
755
813
|
|
|
814
|
+
// ../env/src/files.ts
|
|
815
|
+
import { chmod, chown, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
816
|
+
import { basename, dirname as dirname3, join as join3 } from "path";
|
|
817
|
+
|
|
818
|
+
// ../env/src/dotenv.ts
|
|
819
|
+
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
820
|
+
var INLINE_COMMENT = /\s+#.*$/;
|
|
821
|
+
var CARRIAGE_RETURN = /\r$/;
|
|
822
|
+
var ESCAPES = { n: `
|
|
823
|
+
`, r: "\r", t: "\t", '"': '"', "\\": "\\", $: "$" };
|
|
824
|
+
function closingQuote(text, quote) {
|
|
825
|
+
for (let index = 0;index < text.length; index++) {
|
|
826
|
+
if (quote === '"' && text[index] === "\\")
|
|
827
|
+
index++;
|
|
828
|
+
else if (text[index] === quote)
|
|
829
|
+
return index;
|
|
830
|
+
}
|
|
831
|
+
return -1;
|
|
832
|
+
}
|
|
833
|
+
function unescapeDollar(text) {
|
|
834
|
+
return text.replaceAll("\\$", "$");
|
|
835
|
+
}
|
|
836
|
+
function unescapeDouble(text) {
|
|
837
|
+
let out = "";
|
|
838
|
+
for (let index = 0;index < text.length; index++) {
|
|
839
|
+
const next = text[index + 1];
|
|
840
|
+
if (text[index] === "\\" && next !== undefined && next in ESCAPES) {
|
|
841
|
+
out += ESCAPES[next];
|
|
842
|
+
index++;
|
|
843
|
+
} else {
|
|
844
|
+
out += text[index];
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return out;
|
|
848
|
+
}
|
|
849
|
+
function parseDotenv(text) {
|
|
850
|
+
const physical = text.split(`
|
|
851
|
+
`);
|
|
852
|
+
const lines = [];
|
|
853
|
+
for (let index = 0;index < physical.length; index++) {
|
|
854
|
+
const line = physical[index] ?? "";
|
|
855
|
+
const match = ASSIGNMENT.exec(line.replace(CARRIAGE_RETURN, ""));
|
|
856
|
+
if (match === null) {
|
|
857
|
+
lines.push({ raw: line });
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const [, key = "", rest = ""] = match;
|
|
861
|
+
const start = rest.trimStart();
|
|
862
|
+
const quote = start[0];
|
|
863
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
|
864
|
+
lines.push({ raw: line, key, value: unescapeDollar(start.replace(INLINE_COMMENT, "").trim()) });
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
let body = start.slice(1);
|
|
868
|
+
let raw = line;
|
|
869
|
+
let end = closingQuote(body, quote);
|
|
870
|
+
while (end === -1 && index + 1 < physical.length) {
|
|
871
|
+
index++;
|
|
872
|
+
body += `
|
|
873
|
+
${(physical[index] ?? "").replace(CARRIAGE_RETURN, "")}`;
|
|
874
|
+
raw += `
|
|
875
|
+
${physical[index] ?? ""}`;
|
|
876
|
+
end = closingQuote(body, quote);
|
|
877
|
+
}
|
|
878
|
+
const inner = end === -1 ? body : body.slice(0, end);
|
|
879
|
+
lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : unescapeDollar(inner) });
|
|
880
|
+
}
|
|
881
|
+
return { lines };
|
|
882
|
+
}
|
|
883
|
+
function entries(file) {
|
|
884
|
+
const map = new Map;
|
|
885
|
+
for (const line of file.lines) {
|
|
886
|
+
if (line.key !== undefined)
|
|
887
|
+
map.set(line.key, line.value ?? "");
|
|
888
|
+
}
|
|
889
|
+
return map;
|
|
890
|
+
}
|
|
891
|
+
|
|
756
892
|
// ../env/src/mask.ts
|
|
893
|
+
var MIN_KNOWN_LENGTH = 4;
|
|
757
894
|
var KEY_MASK = "*** private key redacted ***";
|
|
758
895
|
var PLAIN_VALUE = /^(?:true|false|null|none|nil|yes|no|on|off|undefined)$/i;
|
|
759
896
|
var PLACEHOLDER = /^[$%{<(*/]/;
|
|
760
897
|
var URL_PASSWORD = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]*:([^\s'"]{1,1024})@/dgi;
|
|
761
898
|
var PORT = /^\d{1,5}$/;
|
|
899
|
+
var ESCAPE2 = /[.*+?^${}()|[\]\\]/g;
|
|
762
900
|
function isPlaceholder(value) {
|
|
763
901
|
return PLACEHOLDER.test(value) || PLAIN_VALUE.test(value);
|
|
764
902
|
}
|
|
@@ -781,6 +919,7 @@ var TOKEN_FORMATS = [
|
|
|
781
919
|
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
782
920
|
var PEM_END = /-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
783
921
|
var PEM_BODY = /(?<=-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)(?:(?!-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)[\s\S])+/dg;
|
|
922
|
+
var PEM_BEGIN_ALL = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
784
923
|
var URL_PARTS = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
785
924
|
var PATH_SEGMENT = /[^/]+/g;
|
|
786
925
|
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
@@ -795,6 +934,24 @@ function hint(value) {
|
|
|
795
934
|
const shown = Math.min(3, Math.floor(value.length / 4));
|
|
796
935
|
return `${value.slice(0, shown)}***(${value.length})`;
|
|
797
936
|
}
|
|
937
|
+
function forms(value) {
|
|
938
|
+
return [...new Set([value, encodeURIComponent(value), JSON.stringify(value).slice(1, -1)])];
|
|
939
|
+
}
|
|
940
|
+
function knownSecrets(pairs) {
|
|
941
|
+
const keys = new Map;
|
|
942
|
+
for (const [key, value] of pairs) {
|
|
943
|
+
if (value.length < MIN_KNOWN_LENGTH || PLAIN_VALUE.test(value))
|
|
944
|
+
continue;
|
|
945
|
+
for (const form of forms(value)) {
|
|
946
|
+
if (!keys.has(form))
|
|
947
|
+
keys.set(form, key);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (keys.size === 0)
|
|
951
|
+
return { pattern: undefined, keys };
|
|
952
|
+
const alternatives = [...keys.keys()].sort((a, b) => b.length - a.length).map((value) => value.replace(ESCAPE2, "\\$&"));
|
|
953
|
+
return { pattern: new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "g"), keys };
|
|
954
|
+
}
|
|
798
955
|
function passwordSpans(text) {
|
|
799
956
|
const spans = [];
|
|
800
957
|
for (const url of text.matchAll(URL_PASSWORD)) {
|
|
@@ -806,6 +963,9 @@ function passwordSpans(text) {
|
|
|
806
963
|
}
|
|
807
964
|
return spans;
|
|
808
965
|
}
|
|
966
|
+
function urlPasswords(text) {
|
|
967
|
+
return passwordSpans(text).map(([start, end]) => text.slice(start, end));
|
|
968
|
+
}
|
|
809
969
|
function urlTokens(line) {
|
|
810
970
|
const found = [];
|
|
811
971
|
const add = (kind, start, value) => {
|
|
@@ -873,6 +1033,13 @@ function findSecrets(line, known) {
|
|
|
873
1033
|
}
|
|
874
1034
|
return kept;
|
|
875
1035
|
}
|
|
1036
|
+
function openKeyBody(line) {
|
|
1037
|
+
const last = [...line.matchAll(PEM_BEGIN_ALL)].at(-1);
|
|
1038
|
+
if (last === undefined)
|
|
1039
|
+
return;
|
|
1040
|
+
const after = line.slice(last.index + last[0].length);
|
|
1041
|
+
return PEM_END.test(after) ? undefined : after.trim();
|
|
1042
|
+
}
|
|
876
1043
|
function maskLine(line, known) {
|
|
877
1044
|
let out = "";
|
|
878
1045
|
let at = 0;
|
|
@@ -883,26 +1050,139 @@ function maskLine(line, known) {
|
|
|
883
1050
|
return out + line.slice(at);
|
|
884
1051
|
}
|
|
885
1052
|
|
|
1053
|
+
// ../env/src/files.ts
|
|
1054
|
+
var ENV_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.envrc)$/;
|
|
1055
|
+
var KNOWN_DEPTH = 3;
|
|
1056
|
+
var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
|
|
1057
|
+
async function isFileEntry(entry, path) {
|
|
1058
|
+
if (entry.isFile())
|
|
1059
|
+
return true;
|
|
1060
|
+
if (!entry.isSymbolicLink())
|
|
1061
|
+
return false;
|
|
1062
|
+
try {
|
|
1063
|
+
return (await stat(path)).isFile();
|
|
1064
|
+
} catch {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function secretPairs(file) {
|
|
1069
|
+
const pairs = [];
|
|
1070
|
+
for (const [key, value] of entries(file)) {
|
|
1071
|
+
if (isSecretName(key))
|
|
1072
|
+
pairs.push([key, value]);
|
|
1073
|
+
else
|
|
1074
|
+
pairs.push(...urlPasswords(value).map((password) => [key, password]));
|
|
1075
|
+
}
|
|
1076
|
+
return pairs;
|
|
1077
|
+
}
|
|
1078
|
+
async function envFilesBelow(folder, depth = 0) {
|
|
1079
|
+
const found = [];
|
|
1080
|
+
let children;
|
|
1081
|
+
try {
|
|
1082
|
+
children = await readdir(folder, { withFileTypes: true });
|
|
1083
|
+
} catch {
|
|
1084
|
+
return found;
|
|
1085
|
+
}
|
|
1086
|
+
for (const entry of children) {
|
|
1087
|
+
const path = join3(folder, entry.name);
|
|
1088
|
+
if (entry.isDirectory() && depth < KNOWN_DEPTH && !SKIPPED.has(entry.name))
|
|
1089
|
+
found.push(...await envFilesBelow(path, depth + 1));
|
|
1090
|
+
else if (ENV_FILE.test(entry.name) && await isFileEntry(entry, path))
|
|
1091
|
+
found.push(path);
|
|
1092
|
+
}
|
|
1093
|
+
return found;
|
|
1094
|
+
}
|
|
1095
|
+
async function loadKnown(folder = process.cwd(), extra = []) {
|
|
1096
|
+
const pairs = extra.flatMap(secretPairs);
|
|
1097
|
+
for (const path of await envFilesBelow(folder)) {
|
|
1098
|
+
try {
|
|
1099
|
+
pairs.push(...secretPairs(parseDotenv(await Bun.file(path).text())));
|
|
1100
|
+
} catch {}
|
|
1101
|
+
}
|
|
1102
|
+
return knownSecrets(pairs);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/stack.ts
|
|
1106
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
1107
|
+
import { dirname as dirname4, join as join4, relative, resolve } from "path";
|
|
1108
|
+
|
|
1109
|
+
// ../env/src/redactor.ts
|
|
1110
|
+
var MAX_PENDING = 1 << 18;
|
|
1111
|
+
var OVERLAP = 1 << 13;
|
|
1112
|
+
|
|
1113
|
+
class Redactor {
|
|
1114
|
+
known;
|
|
1115
|
+
pending = "";
|
|
1116
|
+
inKey = false;
|
|
1117
|
+
hidden = false;
|
|
1118
|
+
constructor(known) {
|
|
1119
|
+
this.known = known;
|
|
1120
|
+
}
|
|
1121
|
+
push(chunk) {
|
|
1122
|
+
const end = chunk.lastIndexOf(`
|
|
1123
|
+
`);
|
|
1124
|
+
this.pending += chunk;
|
|
1125
|
+
if (end === -1)
|
|
1126
|
+
return this.pending.length > MAX_PENDING ? this.cut() : "";
|
|
1127
|
+
const split = this.pending.length - chunk.length + end + 1;
|
|
1128
|
+
const complete = this.pending.slice(0, split);
|
|
1129
|
+
this.pending = this.pending.slice(split);
|
|
1130
|
+
return complete.split(`
|
|
1131
|
+
`).slice(0, -1).map((line) => this.line(line)).filter((line) => line !== undefined).map((line) => `${line}
|
|
1132
|
+
`).join("");
|
|
1133
|
+
}
|
|
1134
|
+
cut() {
|
|
1135
|
+
const limit = this.pending.length - OVERLAP;
|
|
1136
|
+
const space = this.pending.lastIndexOf(" ", limit);
|
|
1137
|
+
const at = space > 0 ? space + 1 : limit;
|
|
1138
|
+
const head = this.pending.slice(0, at);
|
|
1139
|
+
this.pending = this.pending.slice(at);
|
|
1140
|
+
return this.line(head) ?? "";
|
|
1141
|
+
}
|
|
1142
|
+
end() {
|
|
1143
|
+
const rest = this.pending;
|
|
1144
|
+
this.pending = "";
|
|
1145
|
+
return rest === "" ? "" : this.line(rest) ?? "";
|
|
1146
|
+
}
|
|
1147
|
+
line(line) {
|
|
1148
|
+
if (this.inKey) {
|
|
1149
|
+
const end = PEM_END.exec(line);
|
|
1150
|
+
if (end === null) {
|
|
1151
|
+
if (this.hidden)
|
|
1152
|
+
return;
|
|
1153
|
+
this.hidden = true;
|
|
1154
|
+
return KEY_MASK;
|
|
1155
|
+
}
|
|
1156
|
+
this.inKey = false;
|
|
1157
|
+
return (end.index > 0 && !this.hidden ? KEY_MASK : "") + this.line(line.slice(end.index));
|
|
1158
|
+
}
|
|
1159
|
+
const body = openKeyBody(line);
|
|
1160
|
+
if (body !== undefined) {
|
|
1161
|
+
this.inKey = true;
|
|
1162
|
+
this.hidden = body !== "";
|
|
1163
|
+
}
|
|
1164
|
+
return maskLine(line, this.known);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
886
1168
|
// src/stack.ts
|
|
887
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
888
|
-
import { dirname as dirname2, join as join2, relative, resolve } from "path";
|
|
889
1169
|
var LABEL_FORMAT = ["project", "project.config_files", "project.environment_file", "project.working_dir"].map((label) => `{{.Label "com.docker.compose.${label}"}}`).join("\t");
|
|
890
1170
|
var COMPOSE_FILE_VAR = /^\s*(?:export\s+)?COMPOSE_FILE\s*=\s*\S/m;
|
|
891
1171
|
var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
892
|
-
var
|
|
1172
|
+
var SKIPPED2 = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
|
|
893
1173
|
var ACTIONS = ["shell", "logs", "restart", "up", "stop", "build"];
|
|
894
1174
|
function from(dir, command) {
|
|
895
1175
|
return dir === "." ? command : ["env", "-C", dir, ...command];
|
|
896
1176
|
}
|
|
897
1177
|
function children(dir = ".") {
|
|
898
|
-
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !
|
|
1178
|
+
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED2.has(child.name)).map((child) => child.name).sort();
|
|
899
1179
|
}
|
|
900
1180
|
function up(dir) {
|
|
901
1181
|
const dirs = [];
|
|
902
|
-
for (let current = dir;; current =
|
|
1182
|
+
for (let current = dir;; current = join4(current, "..")) {
|
|
903
1183
|
dirs.push(current);
|
|
904
1184
|
const absolute = resolve(current);
|
|
905
|
-
if (existsSync(
|
|
1185
|
+
if (existsSync(join4(current, ".git")) || dirname4(absolute) === absolute)
|
|
906
1186
|
return dirs;
|
|
907
1187
|
}
|
|
908
1188
|
}
|
|
@@ -910,10 +1190,10 @@ function stackAt(dir) {
|
|
|
910
1190
|
return { dir, run: from(dir, ["docker", "compose"]), list: { argv: ["docker", "compose"], cwd: resolve(dir), env: process.env } };
|
|
911
1191
|
}
|
|
912
1192
|
function holdsStack(dir) {
|
|
913
|
-
if (COMPOSE_FILES.some((name) => existsSync(
|
|
1193
|
+
if (COMPOSE_FILES.some((name) => existsSync(join4(dir, name))))
|
|
914
1194
|
return true;
|
|
915
1195
|
try {
|
|
916
|
-
return COMPOSE_FILE_VAR.test(
|
|
1196
|
+
return COMPOSE_FILE_VAR.test(readFileSync2(join4(dir, ".env"), "utf-8"));
|
|
917
1197
|
} catch {
|
|
918
1198
|
return false;
|
|
919
1199
|
}
|
|
@@ -961,13 +1241,8 @@ async function recordedNearby() {
|
|
|
961
1241
|
return (await labels()).filter((stack) => dirs.has(stack.workdir));
|
|
962
1242
|
}
|
|
963
1243
|
async function attempt(stack, args) {
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
if (code === 0)
|
|
967
|
-
return { text, error: undefined };
|
|
968
|
-
const last = stderr.trim().split(`
|
|
969
|
-
`).at(-1) ?? "";
|
|
970
|
-
return { text: "", error: last === "" ? `exit ${code}` : last };
|
|
1244
|
+
const result = await captureOutput([...stack.list.argv, ...args], { cwd: stack.list.cwd, env: stack.list.env });
|
|
1245
|
+
return result.code === 0 ? { text: result.stdout, error: undefined } : { text: "", error: composeError(result) };
|
|
971
1246
|
}
|
|
972
1247
|
async function output(stack, args) {
|
|
973
1248
|
return (await attempt(stack, args)).text;
|
|
@@ -1011,13 +1286,14 @@ function key(service) {
|
|
|
1011
1286
|
async function shellOf(service) {
|
|
1012
1287
|
return await output(service.stack, ["exec", "-T", service.name, "sh", "-c", "command -v bash"]) ? "bash" : "sh";
|
|
1013
1288
|
}
|
|
1014
|
-
|
|
1289
|
+
var LOGS_TAIL = 200;
|
|
1290
|
+
async function commandFor(service, action, logs = {}) {
|
|
1015
1291
|
const run = service.stack.run;
|
|
1016
1292
|
switch (action) {
|
|
1017
1293
|
case "shell":
|
|
1018
1294
|
return [...run, "exec", service.name, await shellOf(service)];
|
|
1019
1295
|
case "logs":
|
|
1020
|
-
return [...run, "logs", "-f",
|
|
1296
|
+
return [...run, "logs", ...logs.follow === false ? [] : ["-f"], `--tail=${logs.tail ?? LOGS_TAIL}`, service.name];
|
|
1021
1297
|
case "restart":
|
|
1022
1298
|
return [...run, "restart", service.name];
|
|
1023
1299
|
case "up":
|
|
@@ -1043,8 +1319,12 @@ async function serviceNamed(dir, name, project) {
|
|
|
1043
1319
|
const stack = recorded === undefined ? stackAt(dir) : labelledStack(dir, recorded);
|
|
1044
1320
|
return (await services(stack)).find((candidate) => candidate.name === name);
|
|
1045
1321
|
}
|
|
1322
|
+
function maskLog(text, known) {
|
|
1323
|
+
const redactor = new Redactor(known);
|
|
1324
|
+
return redactor.push(text) + redactor.end();
|
|
1325
|
+
}
|
|
1046
1326
|
async function logsOf(stack, name) {
|
|
1047
|
-
return output(stack, ["logs", "--tail=30", "--no-log-prefix", name]);
|
|
1327
|
+
return maskLog(await output(stack, ["logs", "--tail=30", "--no-log-prefix", name]), await loadKnown());
|
|
1048
1328
|
}
|
|
1049
1329
|
|
|
1050
1330
|
// src/dlog.ts
|
|
@@ -1052,15 +1332,21 @@ var usage2 = `dlog [service...]
|
|
|
1052
1332
|
|
|
1053
1333
|
One line per compose service with its state and health, then the last distinct error lines of the services that log some or are not healthy, repeats collapsed to one line with a count and secrets masked as redact does.
|
|
1054
1334
|
With service names, only those are read. The stacks are the ones dx lists: this folder's, its child folders' and the ones only their containers tell of.
|
|
1055
|
-
|
|
1056
|
-
|
|
1335
|
+
\`--level warn\` keeps warnings too, \`--grep <regex>\` keeps the lines matching a case insensitive regex instead of the error words. What fails the exit is \`--fail-on\`: by default a service down, unhealthy or still starting, an error line or logs compose could not read, so it can gate a script; \`--fail-on down\` leaves out an old error line that \`--since\` has not aged out yet. A service that exited with code 0 (a migration, an init) is not down.`;
|
|
1336
|
+
var EXITS = [[0, "nothing the --fail-on list names: every service is up and quiet"], [1, "a service is down, unhealthy or still starting, logged a matching line or has logs compose could not read (as --fail-on keeps them), or there is no compose service here"], [2, "a usage error"]];
|
|
1337
|
+
var FAIL_ON = ["down", "unhealthy", "errors"];
|
|
1057
1338
|
var options = {
|
|
1058
1339
|
lines: { type: "string", short: "n", default: "5", description: "error lines kept per service" },
|
|
1059
1340
|
since: { type: "string", default: "1h", description: "how far back the logs are read (compose --since: 30m, 2h, a timestamp)" },
|
|
1341
|
+
level: { type: "string", choices: ["error", "warn"], default: "error", description: "lowest level of the lines kept: error (the default) or warn, warnings too" },
|
|
1342
|
+
grep: { type: "string", description: "keep the lines matching this regex, case insensitive, instead of the --level words" },
|
|
1343
|
+
"fail-on": { type: "string", multiple: true, choices: FAIL_ON, description: "what makes the exit 1, repeatable: down, unhealthy or errors (all three)" },
|
|
1060
1344
|
all: { type: "boolean", short: "a", description: "the last log lines of a service with no error too" },
|
|
1061
|
-
json: jsonOption("[{ key, state, health, status, errors: [{ line, count }], tail }]")
|
|
1345
|
+
json: jsonOption("[{ key, state, health, status, errors: [{ line, count }], tail, down, unhealthy }]")
|
|
1062
1346
|
};
|
|
1063
1347
|
var ERROR = /\b(?:error|err|fatal|exception|panic|critical|crit|traceback|failed|failure|refused|denied|timeout|timed out|oom|killed)\b/i;
|
|
1348
|
+
var WARNING = /\b(?:warn|warning|deprecated|deprecation)\b/i;
|
|
1349
|
+
var LEVELS = { error: ERROR, warn: new RegExp(`${ERROR.source}|${WARNING.source}`, "i") };
|
|
1064
1350
|
var TAIL = 500;
|
|
1065
1351
|
var MAX_WIDTH = 220;
|
|
1066
1352
|
var TIMESTAMP = /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?|\d{2}[/-][A-Za-z]{3}[/-]\d{4}:\d{2}:\d{2}:\d{2}(?: [+-]\d{4})?|\d{2}:\d{2}:\d{2}(?:[.,]\d+)?/g;
|
|
@@ -1074,24 +1360,38 @@ function shape(line) {
|
|
|
1074
1360
|
function clip(line) {
|
|
1075
1361
|
return line.length > MAX_WIDTH ? `${line.slice(0, MAX_WIDTH - 1)}\u2026` : line;
|
|
1076
1362
|
}
|
|
1077
|
-
function errorLines(log, limit) {
|
|
1363
|
+
function errorLines(log, limit, known, matches = ERROR) {
|
|
1078
1364
|
const seen = new Map;
|
|
1079
1365
|
for (const raw of log.split(`
|
|
1080
1366
|
`)) {
|
|
1081
1367
|
const line = stripAnsi(raw).trim();
|
|
1082
|
-
if (line === "" || !
|
|
1368
|
+
if (line === "" || !matches.test(line))
|
|
1083
1369
|
continue;
|
|
1084
1370
|
const shared = shape(line);
|
|
1085
1371
|
const before = seen.get(shared);
|
|
1086
1372
|
seen.delete(shared);
|
|
1087
|
-
seen.set(shared, { line: clip(maskLine(line)), count: (before?.count ?? 0) + 1 });
|
|
1373
|
+
seen.set(shared, { line: clip(maskLine(line, known)), count: (before?.count ?? 0) + 1 });
|
|
1088
1374
|
}
|
|
1089
1375
|
return [...seen.values()].slice(-limit);
|
|
1090
1376
|
}
|
|
1377
|
+
function isUp(state, container) {
|
|
1378
|
+
return state === "running" || state === "exited" && container?.ExitCode === 0;
|
|
1379
|
+
}
|
|
1091
1380
|
function healthOf(state, container) {
|
|
1092
1381
|
const health = (container?.Health ?? "") || (STATUS_HEALTH.exec(container?.Status ?? "")?.[1]?.replace("health: ", "") ?? "");
|
|
1093
|
-
|
|
1094
|
-
|
|
1382
|
+
return { health, fine: isUp(state, container) && (health === "" || health === "healthy") };
|
|
1383
|
+
}
|
|
1384
|
+
function needsLook(report, failOn) {
|
|
1385
|
+
return failOn.has("down") && (report.down || report.unreadable !== undefined) || failOn.has("unhealthy") && report.unhealthy || failOn.has("errors") && (report.errors.length > 0 || report.unreadable !== undefined);
|
|
1386
|
+
}
|
|
1387
|
+
function matcherOf(level, grep) {
|
|
1388
|
+
if (grep === undefined)
|
|
1389
|
+
return LEVELS[level];
|
|
1390
|
+
try {
|
|
1391
|
+
return new RegExp(grep, "i");
|
|
1392
|
+
} catch {
|
|
1393
|
+
return badUsage(`--grep takes a regex, ${grep} is not one`);
|
|
1394
|
+
}
|
|
1095
1395
|
}
|
|
1096
1396
|
async function read(service, since) {
|
|
1097
1397
|
const [ps, logs] = await Promise.all([
|
|
@@ -1128,10 +1428,11 @@ function show(reports) {
|
|
|
1128
1428
|
}
|
|
1129
1429
|
}
|
|
1130
1430
|
async function dlog(args) {
|
|
1131
|
-
const parsed = parse(args, usage2, options, { replaces: "docker compose ps, logs --tail | grep -i error and docker inspect for health", examples: ["dlog", ["dlog php db", "only those services"], ["dlog -n 10 --since 15m", "ten error lines of the last quarter of an hour"], ["dlog --json", "the report as JSON"]] });
|
|
1132
|
-
const limit =
|
|
1133
|
-
|
|
1134
|
-
|
|
1431
|
+
const parsed = parse(args, usage2, options, { exits: EXITS, replaces: "docker compose ps, logs --tail | grep -i error and docker inspect for health", examples: ["dlog", ["dlog php db", "only those services"], ["dlog -n 10 --since 15m", "ten error lines of the last quarter of an hour"], ["dlog --level warn", "warnings too"], ['dlog --grep "timeout|5\\d\\d"', "the lines matching a regex"], ["dlog --fail-on down --fail-on unhealthy", "a gate that ignores old errors"], ["dlog --json", "the report as JSON"]] });
|
|
1432
|
+
const limit = wholeNumber(parsed.values.lines, "lines", 5);
|
|
1433
|
+
const matches = matcherOf(parsed.values.level === "warn" ? "warn" : "error", parsed.values.grep);
|
|
1434
|
+
const failOn = new Set((parsed.values["fail-on"] ?? []).length > 0 ? parsed.values["fail-on"] ?? FAIL_ON : FAIL_ON);
|
|
1435
|
+
const known = await loadKnown();
|
|
1135
1436
|
const services = await allServices();
|
|
1136
1437
|
if (services.length === 0)
|
|
1137
1438
|
fail("no compose service here", 1);
|
|
@@ -1146,19 +1447,19 @@ async function dlog(args) {
|
|
|
1146
1447
|
const lines = withoutKeys(log.split(`
|
|
1147
1448
|
`));
|
|
1148
1449
|
const errors = errorLines(lines.join(`
|
|
1149
|
-
`), limit);
|
|
1150
|
-
const tail = parsed.values.all === true && errors.length === 0 ? lines.map((text) => clip(maskLine(stripAnsi(text).trim()))).filter(Boolean).slice(-limit) : [];
|
|
1151
|
-
return { key: key(service), state: service.state, health, status: service.status, errors, tail, unreadable, fine };
|
|
1450
|
+
`), limit, known, matches);
|
|
1451
|
+
const tail = parsed.values.all === true && errors.length === 0 ? lines.map((text) => clip(maskLine(stripAnsi(text).trim(), known))).filter(Boolean).slice(-limit) : [];
|
|
1452
|
+
return { key: key(service), state: service.state, health, status: service.status, errors, tail, unreadable, fine, down: !isUp(service.state, container), unhealthy: isUp(service.state, container) && !fine };
|
|
1152
1453
|
}));
|
|
1153
1454
|
if (parsed.values.json === true)
|
|
1154
1455
|
printJson(reports);
|
|
1155
1456
|
else
|
|
1156
1457
|
show(reports);
|
|
1157
|
-
const down = reports.filter((report) =>
|
|
1458
|
+
const down = reports.filter((report) => needsLook(report, failOn));
|
|
1158
1459
|
if (parsed.values.json !== true && down.length > 0)
|
|
1159
1460
|
console.error(paint(process8.stderr).dim(`${down.length} of ${reports.length} services need a look: ${down.map((report) => report.key).join(", ")}`));
|
|
1160
1461
|
return down.length > 0 ? 1 : 0;
|
|
1161
1462
|
}
|
|
1162
1463
|
|
|
1163
1464
|
// src/dlog-cli.ts
|
|
1164
|
-
process9.exitCode = await runMain({ name: "dlog",
|
|
1465
|
+
process9.exitCode = await runMain({ name: "dlog", run: dlog });
|