@kaynooo/cli-docker 0.3.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 +6 -3
- package/dist/dc-cli.js +448 -27
- package/dist/dlog-cli.js +1465 -0
- package/dist/dx-cli.js +479 -42
- package/dist/sf-cli.js +494 -50
- package/package.json +6 -3
- package/src/dc-cli.ts +1 -2
- package/src/dlog-cli.ts +7 -0
- package/src/dlog.ts +219 -0
- 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 +33 -7
package/dist/dx-cli.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
// src/dx-cli.ts
|
|
5
5
|
import process11 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)) {
|
|
@@ -349,6 +356,9 @@ function optionRows(options, colors = paint(process3.stdout)) {
|
|
|
349
356
|
|
|
350
357
|
// ../kit/src/text.ts
|
|
351
358
|
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
359
|
+
function stripAnsi(text) {
|
|
360
|
+
return text.replace(ANSI, "");
|
|
361
|
+
}
|
|
352
362
|
function distance(a, b) {
|
|
353
363
|
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
354
364
|
for (const [i, left] of [...a].entries()) {
|
|
@@ -366,6 +376,16 @@ function closest(name, names) {
|
|
|
366
376
|
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
367
377
|
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
368
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
|
+
}
|
|
369
389
|
|
|
370
390
|
// ../kit/src/args.ts
|
|
371
391
|
var DASHES = /^-+/;
|
|
@@ -439,7 +459,7 @@ function parse(args, usage, options, settings = {}) {
|
|
|
439
459
|
const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
|
|
440
460
|
let parsed;
|
|
441
461
|
try {
|
|
442
|
-
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 });
|
|
443
463
|
} catch (error) {
|
|
444
464
|
const message = parseError(error);
|
|
445
465
|
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
@@ -447,7 +467,7 @@ function parse(args, usage, options, settings = {}) {
|
|
|
447
467
|
const values = parsed.values;
|
|
448
468
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
449
469
|
if (values.help === true) {
|
|
450
|
-
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));
|
|
451
471
|
throw new HelpShown;
|
|
452
472
|
}
|
|
453
473
|
checkChoices(parsed.values, all);
|
|
@@ -489,6 +509,18 @@ function listed(options, allowNegative) {
|
|
|
489
509
|
return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
|
|
490
510
|
}));
|
|
491
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
|
+
}
|
|
492
524
|
function withBare(args, options) {
|
|
493
525
|
const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
|
|
494
526
|
if (bare.size === 0)
|
|
@@ -579,6 +611,8 @@ var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x
|
|
|
579
611
|
var RESET_STYLE = `${ESC}[0m`;
|
|
580
612
|
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
581
613
|
// ../kit/src/cli.ts
|
|
614
|
+
import { readFileSync, realpathSync } from "fs";
|
|
615
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
582
616
|
import process6 from "process";
|
|
583
617
|
|
|
584
618
|
// ../kit/src/usage.ts
|
|
@@ -721,11 +755,22 @@ async function printCandidates(found) {
|
|
|
721
755
|
console.log(line);
|
|
722
756
|
return 0;
|
|
723
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
|
+
}
|
|
724
769
|
async function runMain({ name, version, run, redact, usage }, argv = process6.argv.slice(2)) {
|
|
725
770
|
if (argv[0] === COMPLETE)
|
|
726
771
|
return printCandidates(leafCandidates(run, argv.slice(1)));
|
|
727
772
|
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
728
|
-
console.log(version);
|
|
773
|
+
console.log(version ?? packageVersion());
|
|
729
774
|
return 0;
|
|
730
775
|
}
|
|
731
776
|
let code;
|
|
@@ -734,26 +779,392 @@ async function runMain({ name, version, run, redact, usage }, argv = process6.ar
|
|
|
734
779
|
} catch (error) {
|
|
735
780
|
code = reportThrown(name, error, redact, wantsJson(argv));
|
|
736
781
|
}
|
|
737
|
-
if (usage !== false)
|
|
738
|
-
|
|
782
|
+
if (usage !== false) {
|
|
783
|
+
try {
|
|
784
|
+
recordUsage(usageEntry(name, undefined, argv, code));
|
|
785
|
+
} catch {}
|
|
786
|
+
}
|
|
739
787
|
return code;
|
|
740
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
|
+
}
|
|
741
800
|
// ../kit/src/secrets.ts
|
|
801
|
+
var SECRET_PART = /token|secret|passw|credential|private|dsn|apikey|accesskey|key$/i;
|
|
742
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
|
+
}
|
|
743
807
|
// ../kit/src/sessions.ts
|
|
744
808
|
var {Glob } = globalThis.Bun;
|
|
745
809
|
var SESSION_FILES = new Glob("*.json");
|
|
746
810
|
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
747
|
-
// package.json
|
|
748
|
-
var version = "0.3.0";
|
|
749
|
-
|
|
750
811
|
// src/dx.ts
|
|
751
812
|
import process10 from "process";
|
|
752
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
|
+
|
|
892
|
+
// ../env/src/mask.ts
|
|
893
|
+
var MIN_KNOWN_LENGTH = 4;
|
|
894
|
+
var KEY_MASK = "*** private key redacted ***";
|
|
895
|
+
var PLAIN_VALUE = /^(?:true|false|null|none|nil|yes|no|on|off|undefined)$/i;
|
|
896
|
+
var PLACEHOLDER = /^[$%{<(*/]/;
|
|
897
|
+
var URL_PASSWORD = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]*:([^\s'"]{1,1024})@/dgi;
|
|
898
|
+
var PORT = /^\d{1,5}$/;
|
|
899
|
+
var ESCAPE2 = /[.*+?^${}()|[\]\\]/g;
|
|
900
|
+
function isPlaceholder(value) {
|
|
901
|
+
return PLACEHOLDER.test(value) || PLAIN_VALUE.test(value);
|
|
902
|
+
}
|
|
903
|
+
var TOKEN_FORMATS = [
|
|
904
|
+
{ kind: "GitHub token", pattern: /\b(?:gh[pousr]_|github_pat_)\w{20,}/dg },
|
|
905
|
+
{ kind: "GitLab token", pattern: /\bglpat-[\w-]{20,}/dg },
|
|
906
|
+
{ kind: "API key", pattern: /\bsk-[\w-]{20,}/dg },
|
|
907
|
+
{ kind: "Slack token", pattern: /\bx(?:ox[abprs]|app)-[\w-]{10,}/dg },
|
|
908
|
+
{ kind: "JWT", pattern: /\beyJ[\w-]+\.[\w-]+\.[\w-]+/dg },
|
|
909
|
+
{ kind: "AWS access key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/dg },
|
|
910
|
+
{ kind: "npm token", pattern: /\bnpm_[A-Za-z0-9]{30,}/dg },
|
|
911
|
+
{ kind: "Stripe key", pattern: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{10,}/dg },
|
|
912
|
+
{ kind: "Google API key", pattern: /\bAIza[\w-]{35}/dg },
|
|
913
|
+
{ kind: "SendGrid key", pattern: /\bSG\.[\w-]{16,}\.[\w-]{16,}/dg },
|
|
914
|
+
{ kind: "Authorization header", pattern: /\bAuthorization["']?\s*[:=]\s*["']?(?:Bearer|Basic|Token|Digest)\s+([^\s"',;]{8,})/dgi, skip: isPlaceholder },
|
|
915
|
+
{ kind: "Bearer token", pattern: /\bBearer\s+((?=[\w.~+/=-]*\d)[\w.~+/=-]{20,})/dg },
|
|
916
|
+
{ kind: "password", pattern: /(?<![\w-])(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?=([^\s"'&;,]{4,})/dgi, skip: isPlaceholder },
|
|
917
|
+
{ kind: "password", pattern: /(?<![\w-])["']?(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?["']?\s*[:=]\s*(["'])((?:\\.|(?!\1)[^\\\n]){4,})\1/dgi, skip: isPlaceholder }
|
|
918
|
+
];
|
|
919
|
+
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
920
|
+
var PEM_END = /-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
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;
|
|
923
|
+
var URL_PARTS = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
924
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
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;
|
|
926
|
+
var WEBHOOK_FORMATS = [
|
|
927
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
928
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
929
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
930
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
931
|
+
];
|
|
932
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
933
|
+
function hint(value) {
|
|
934
|
+
const shown = Math.min(3, Math.floor(value.length / 4));
|
|
935
|
+
return `${value.slice(0, shown)}***(${value.length})`;
|
|
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
|
+
}
|
|
955
|
+
function passwordSpans(text) {
|
|
956
|
+
const spans = [];
|
|
957
|
+
for (const url of text.matchAll(URL_PASSWORD)) {
|
|
958
|
+
const [start, end] = url.indices?.[1] ?? [];
|
|
959
|
+
const password = url[1] ?? "";
|
|
960
|
+
const slash = password.indexOf("/");
|
|
961
|
+
if (start !== undefined && end !== undefined && !(slash !== -1 && PORT.test(password.slice(0, slash))))
|
|
962
|
+
spans.push([start, end]);
|
|
963
|
+
}
|
|
964
|
+
return spans;
|
|
965
|
+
}
|
|
966
|
+
function urlPasswords(text) {
|
|
967
|
+
return passwordSpans(text).map(([start, end]) => text.slice(start, end));
|
|
968
|
+
}
|
|
969
|
+
function urlTokens(line) {
|
|
970
|
+
const found = [];
|
|
971
|
+
const add = (kind, start, value) => {
|
|
972
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
973
|
+
};
|
|
974
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
975
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
976
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
977
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
978
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
979
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
980
|
+
continue;
|
|
981
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
982
|
+
const target = authority.slice(hostAt) + path;
|
|
983
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
984
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
985
|
+
if (start !== undefined && end !== undefined)
|
|
986
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
987
|
+
}
|
|
988
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
989
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
990
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
991
|
+
}
|
|
992
|
+
if (query === undefined || queryStart === undefined)
|
|
993
|
+
continue;
|
|
994
|
+
let offset = 0;
|
|
995
|
+
for (const param of query.split("&")) {
|
|
996
|
+
const equals = param.indexOf("=");
|
|
997
|
+
if (equals > 0 && equals < param.length - 1 && SECRET_PARAM.test(param.slice(0, equals)))
|
|
998
|
+
add("URL token", queryStart + offset + equals + 1, param.slice(equals + 1));
|
|
999
|
+
offset += param.length + 1;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return found.filter((match) => match.end > match.start);
|
|
1003
|
+
}
|
|
1004
|
+
function findSecrets(line, known) {
|
|
1005
|
+
const found = [];
|
|
1006
|
+
for (const [start, end] of passwordSpans(line))
|
|
1007
|
+
found.push({ start, end, kind: "URL password", mask: "***" });
|
|
1008
|
+
for (const body of line.matchAll(PEM_BODY)) {
|
|
1009
|
+
const [start, end] = body.indices?.[0] ?? [];
|
|
1010
|
+
if (start !== undefined && end !== undefined)
|
|
1011
|
+
found.push({ start, end, kind: "private key", mask: KEY_MASK });
|
|
1012
|
+
}
|
|
1013
|
+
for (const { kind, pattern, skip } of TOKEN_FORMATS) {
|
|
1014
|
+
for (const token of line.matchAll(pattern)) {
|
|
1015
|
+
const [start, end] = token.indices?.at(-1) ?? [];
|
|
1016
|
+
if (start === undefined || end === undefined)
|
|
1017
|
+
continue;
|
|
1018
|
+
const value = line.slice(start, end);
|
|
1019
|
+
if (!skip?.(value))
|
|
1020
|
+
found.push({ start, end, kind, mask: hint(value) });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
found.push(...urlTokens(line));
|
|
1024
|
+
if (known?.pattern !== undefined) {
|
|
1025
|
+
for (const value of line.matchAll(known.pattern))
|
|
1026
|
+
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
1027
|
+
}
|
|
1028
|
+
found.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1029
|
+
const kept = [];
|
|
1030
|
+
for (const match of found) {
|
|
1031
|
+
if (match.start >= (kept.at(-1)?.end ?? 0))
|
|
1032
|
+
kept.push(match);
|
|
1033
|
+
}
|
|
1034
|
+
return kept;
|
|
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
|
+
}
|
|
1043
|
+
function maskLine(line, known) {
|
|
1044
|
+
let out = "";
|
|
1045
|
+
let at = 0;
|
|
1046
|
+
for (const match of findSecrets(line, known)) {
|
|
1047
|
+
out += line.slice(at, match.start) + match.mask;
|
|
1048
|
+
at = match.end;
|
|
1049
|
+
}
|
|
1050
|
+
return out + line.slice(at);
|
|
1051
|
+
}
|
|
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
|
+
// ../env/src/redactor.ts
|
|
1106
|
+
var MAX_PENDING = 1 << 18;
|
|
1107
|
+
var OVERLAP = 1 << 13;
|
|
1108
|
+
|
|
1109
|
+
class Redactor {
|
|
1110
|
+
known;
|
|
1111
|
+
pending = "";
|
|
1112
|
+
inKey = false;
|
|
1113
|
+
hidden = false;
|
|
1114
|
+
constructor(known) {
|
|
1115
|
+
this.known = known;
|
|
1116
|
+
}
|
|
1117
|
+
push(chunk) {
|
|
1118
|
+
const end = chunk.lastIndexOf(`
|
|
1119
|
+
`);
|
|
1120
|
+
this.pending += chunk;
|
|
1121
|
+
if (end === -1)
|
|
1122
|
+
return this.pending.length > MAX_PENDING ? this.cut() : "";
|
|
1123
|
+
const split = this.pending.length - chunk.length + end + 1;
|
|
1124
|
+
const complete = this.pending.slice(0, split);
|
|
1125
|
+
this.pending = this.pending.slice(split);
|
|
1126
|
+
return complete.split(`
|
|
1127
|
+
`).slice(0, -1).map((line) => this.line(line)).filter((line) => line !== undefined).map((line) => `${line}
|
|
1128
|
+
`).join("");
|
|
1129
|
+
}
|
|
1130
|
+
cut() {
|
|
1131
|
+
const limit = this.pending.length - OVERLAP;
|
|
1132
|
+
const space = this.pending.lastIndexOf(" ", limit);
|
|
1133
|
+
const at = space > 0 ? space + 1 : limit;
|
|
1134
|
+
const head = this.pending.slice(0, at);
|
|
1135
|
+
this.pending = this.pending.slice(at);
|
|
1136
|
+
return this.line(head) ?? "";
|
|
1137
|
+
}
|
|
1138
|
+
end() {
|
|
1139
|
+
const rest = this.pending;
|
|
1140
|
+
this.pending = "";
|
|
1141
|
+
return rest === "" ? "" : this.line(rest) ?? "";
|
|
1142
|
+
}
|
|
1143
|
+
line(line) {
|
|
1144
|
+
if (this.inKey) {
|
|
1145
|
+
const end = PEM_END.exec(line);
|
|
1146
|
+
if (end === null) {
|
|
1147
|
+
if (this.hidden)
|
|
1148
|
+
return;
|
|
1149
|
+
this.hidden = true;
|
|
1150
|
+
return KEY_MASK;
|
|
1151
|
+
}
|
|
1152
|
+
this.inKey = false;
|
|
1153
|
+
return (end.index > 0 && !this.hidden ? KEY_MASK : "") + this.line(line.slice(end.index));
|
|
1154
|
+
}
|
|
1155
|
+
const body = openKeyBody(line);
|
|
1156
|
+
if (body !== undefined) {
|
|
1157
|
+
this.inKey = true;
|
|
1158
|
+
this.hidden = body !== "";
|
|
1159
|
+
}
|
|
1160
|
+
return maskLine(line, this.known);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
753
1164
|
// ../kit/src/fzf.ts
|
|
754
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
1165
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
755
1166
|
import { homedir as homedir2 } from "os";
|
|
756
|
-
import { dirname as
|
|
1167
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
757
1168
|
import process8 from "process";
|
|
758
1169
|
var REMEMBERED = 20;
|
|
759
1170
|
function table(rows) {
|
|
@@ -809,12 +1220,12 @@ function heightFor(count) {
|
|
|
809
1220
|
return String(Math.max(15, Math.ceil((count + 4) / 0.6)));
|
|
810
1221
|
}
|
|
811
1222
|
function historyFile(name) {
|
|
812
|
-
return
|
|
1223
|
+
return join4(process8.env.XDG_CACHE_HOME ?? join4(homedir2(), ".cache"), name, "history.json");
|
|
813
1224
|
}
|
|
814
1225
|
function history(name) {
|
|
815
1226
|
try {
|
|
816
1227
|
const file = historyFile(name);
|
|
817
|
-
return existsSync(file) ? JSON.parse(
|
|
1228
|
+
return existsSync(file) ? JSON.parse(readFileSync2(file, "utf-8")) : {};
|
|
818
1229
|
} catch {
|
|
819
1230
|
return {};
|
|
820
1231
|
}
|
|
@@ -826,7 +1237,7 @@ function record(name, run) {
|
|
|
826
1237
|
const all = history(name);
|
|
827
1238
|
const file = historyFile(name);
|
|
828
1239
|
all[process8.cwd()] = [run, ...(all[process8.cwd()] ?? []).filter((earlier) => earlier.id !== run.id)].slice(0, REMEMBERED);
|
|
829
|
-
mkdirSync2(
|
|
1240
|
+
mkdirSync2(dirname4(file), { recursive: true });
|
|
830
1241
|
writeFileSync(file, JSON.stringify(all));
|
|
831
1242
|
}
|
|
832
1243
|
function byRecency(items, previous, idOf) {
|
|
@@ -852,25 +1263,25 @@ function list(rows) {
|
|
|
852
1263
|
import process9 from "process";
|
|
853
1264
|
|
|
854
1265
|
// src/stack.ts
|
|
855
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
856
|
-
import { dirname as
|
|
1266
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
1267
|
+
import { dirname as dirname5, join as join5, relative, resolve } from "path";
|
|
857
1268
|
var LABEL_FORMAT = ["project", "project.config_files", "project.environment_file", "project.working_dir"].map((label) => `{{.Label "com.docker.compose.${label}"}}`).join("\t");
|
|
858
1269
|
var COMPOSE_FILE_VAR = /^\s*(?:export\s+)?COMPOSE_FILE\s*=\s*\S/m;
|
|
859
1270
|
var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
860
|
-
var
|
|
1271
|
+
var SKIPPED2 = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
|
|
861
1272
|
var ACTIONS = ["shell", "logs", "restart", "up", "stop", "build"];
|
|
862
1273
|
function from(dir, command) {
|
|
863
1274
|
return dir === "." ? command : ["env", "-C", dir, ...command];
|
|
864
1275
|
}
|
|
865
1276
|
function children(dir = ".") {
|
|
866
|
-
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !
|
|
1277
|
+
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED2.has(child.name)).map((child) => child.name).sort();
|
|
867
1278
|
}
|
|
868
1279
|
function up(dir) {
|
|
869
1280
|
const dirs = [];
|
|
870
|
-
for (let current = dir;; current =
|
|
1281
|
+
for (let current = dir;; current = join5(current, "..")) {
|
|
871
1282
|
dirs.push(current);
|
|
872
1283
|
const absolute = resolve(current);
|
|
873
|
-
if (existsSync2(
|
|
1284
|
+
if (existsSync2(join5(current, ".git")) || dirname5(absolute) === absolute)
|
|
874
1285
|
return dirs;
|
|
875
1286
|
}
|
|
876
1287
|
}
|
|
@@ -878,10 +1289,10 @@ function stackAt(dir) {
|
|
|
878
1289
|
return { dir, run: from(dir, ["docker", "compose"]), list: { argv: ["docker", "compose"], cwd: resolve(dir), env: process.env } };
|
|
879
1290
|
}
|
|
880
1291
|
function holdsStack(dir) {
|
|
881
|
-
if (COMPOSE_FILES.some((name) => existsSync2(
|
|
1292
|
+
if (COMPOSE_FILES.some((name) => existsSync2(join5(dir, name))))
|
|
882
1293
|
return true;
|
|
883
1294
|
try {
|
|
884
|
-
return COMPOSE_FILE_VAR.test(
|
|
1295
|
+
return COMPOSE_FILE_VAR.test(readFileSync3(join5(dir, ".env"), "utf-8"));
|
|
885
1296
|
} catch {
|
|
886
1297
|
return false;
|
|
887
1298
|
}
|
|
@@ -928,9 +1339,12 @@ async function recordedNearby() {
|
|
|
928
1339
|
const dirs = new Set([...up("."), ...children()].map((dir) => resolve(dir)));
|
|
929
1340
|
return (await labels()).filter((stack) => dirs.has(stack.workdir));
|
|
930
1341
|
}
|
|
1342
|
+
async function attempt(stack, args) {
|
|
1343
|
+
const result = await captureOutput([...stack.list.argv, ...args], { cwd: stack.list.cwd, env: stack.list.env });
|
|
1344
|
+
return result.code === 0 ? { text: result.stdout, error: undefined } : { text: "", error: composeError(result) };
|
|
1345
|
+
}
|
|
931
1346
|
async function output(stack, args) {
|
|
932
|
-
|
|
933
|
-
return await proc.exited === 0 ? new Response(proc.stdout).text() : "";
|
|
1347
|
+
return (await attempt(stack, args)).text;
|
|
934
1348
|
}
|
|
935
1349
|
function containers(text) {
|
|
936
1350
|
const trimmed = text.trim();
|
|
@@ -971,13 +1385,14 @@ function key(service) {
|
|
|
971
1385
|
async function shellOf(service) {
|
|
972
1386
|
return await output(service.stack, ["exec", "-T", service.name, "sh", "-c", "command -v bash"]) ? "bash" : "sh";
|
|
973
1387
|
}
|
|
974
|
-
|
|
1388
|
+
var LOGS_TAIL = 200;
|
|
1389
|
+
async function commandFor(service, action, logs = {}) {
|
|
975
1390
|
const run = service.stack.run;
|
|
976
1391
|
switch (action) {
|
|
977
1392
|
case "shell":
|
|
978
1393
|
return [...run, "exec", service.name, await shellOf(service)];
|
|
979
1394
|
case "logs":
|
|
980
|
-
return [...run, "logs", "-f",
|
|
1395
|
+
return [...run, "logs", ...logs.follow === false ? [] : ["-f"], `--tail=${logs.tail ?? LOGS_TAIL}`, service.name];
|
|
981
1396
|
case "restart":
|
|
982
1397
|
return [...run, "restart", service.name];
|
|
983
1398
|
case "up":
|
|
@@ -1003,8 +1418,12 @@ async function serviceNamed(dir, name, project) {
|
|
|
1003
1418
|
const stack = recorded === undefined ? stackAt(dir) : labelledStack(dir, recorded);
|
|
1004
1419
|
return (await services(stack)).find((candidate) => candidate.name === name);
|
|
1005
1420
|
}
|
|
1421
|
+
function maskLog(text, known) {
|
|
1422
|
+
const redactor = new Redactor(known);
|
|
1423
|
+
return redactor.push(text) + redactor.end();
|
|
1424
|
+
}
|
|
1006
1425
|
async function logsOf(stack, name) {
|
|
1007
|
-
return output(stack, ["logs", "--tail=30", "--no-log-prefix", name]);
|
|
1426
|
+
return maskLog(await output(stack, ["logs", "--tail=30", "--no-log-prefix", name]), await loadKnown());
|
|
1008
1427
|
}
|
|
1009
1428
|
|
|
1010
1429
|
// src/picker.ts
|
|
@@ -1037,13 +1456,14 @@ function pickService(services) {
|
|
|
1037
1456
|
var usage2 = `dx [service] [action]
|
|
1038
1457
|
dx -
|
|
1039
1458
|
|
|
1040
|
-
Without a
|
|
1041
|
-
\`dx -\` does the last action again, \`dx <service> [action]\` acts without the picker: \`shell\` (bash when the container has it), \`logs\` (follows the last
|
|
1042
|
-
The services are the stack of this folder or its nearest parent, the ones of the child folders (keyed \`api/php\`), and stacks found only through their containers (keyed \`<project>:php\`)
|
|
1043
|
-
|
|
1044
|
-
Exits with the code of the command it ran, 1 when there is no compose service here or none by that name.`;
|
|
1459
|
+
Without a terminal or without fzf (a script, a pipe, an agent) \`dx\` lists the services with their state, ports and image: that listing is the form to use there, \`--json\` giving the full record. On a terminal with fzf it picks one instead: enter opens its shell (or brings a stopped service up), ctrl-l follows its logs, ctrl-r restarts, ctrl-x stops and ctrl-b rebuilds it. The services last acted on come first, per folder.
|
|
1460
|
+
\`dx -\` does the last action again, \`dx <service> [action]\` acts without the picker: \`shell\` (bash when the container has it), \`logs\` (follows the last ${LOGS_TAIL} lines; \`--tail N\` changes the count and \`--no-follow\` prints them once and exits, for a script), \`restart\`, \`up\`, \`stop\` or \`build\` (up with a rebuild).
|
|
1461
|
+
The services are the stack of this folder or its nearest parent, the ones of the child folders (keyed \`api/php\`), and stacks found only through their containers (keyed \`<project>:php\`).`;
|
|
1462
|
+
var EXITS = [["the code of the command it ran", "a listing, a shell, logs, restart, up, stop or build"], [0, "it listed the services"], [1, "no compose service here or none by that name"], [2, "a usage error"]];
|
|
1045
1463
|
var options = {
|
|
1046
|
-
json: jsonOption("[{ key, project, name, state, status, ports, image }]")
|
|
1464
|
+
json: jsonOption("[{ key, project, name, state, status, ports, image }]"),
|
|
1465
|
+
tail: { type: "string", description: `log lines shown by the logs action (${LOGS_TAIL})` },
|
|
1466
|
+
follow: { type: "boolean", negative: "print the logs once and exit instead of following them" }
|
|
1047
1467
|
};
|
|
1048
1468
|
function table2(services) {
|
|
1049
1469
|
const colors = paint(process10.stdout);
|
|
@@ -1097,6 +1517,20 @@ async function hidden(flag, rest, services) {
|
|
|
1097
1517
|
}
|
|
1098
1518
|
return;
|
|
1099
1519
|
}
|
|
1520
|
+
function maskedLogs(command, redactPath = Bun.which("redact")) {
|
|
1521
|
+
if (redactPath === null)
|
|
1522
|
+
fail("redact is not installed, so the logs would not be masked", 1);
|
|
1523
|
+
return ["sh", "-c", '"$@" | redact', "sh", ...command];
|
|
1524
|
+
}
|
|
1525
|
+
async function followMasked(command) {
|
|
1526
|
+
const redactor = new Redactor(await loadKnown());
|
|
1527
|
+
const decoder = new TextDecoder;
|
|
1528
|
+
const proc = Bun.spawn(command, { stdio: ["inherit", "pipe", "inherit"] });
|
|
1529
|
+
for await (const chunk of proc.stdout)
|
|
1530
|
+
process10.stdout.write(redactor.push(decoder.decode(chunk, { stream: true })));
|
|
1531
|
+
process10.stdout.write(redactor.end());
|
|
1532
|
+
return proc.exited;
|
|
1533
|
+
}
|
|
1100
1534
|
async function dx(args) {
|
|
1101
1535
|
const [flag = ""] = args;
|
|
1102
1536
|
if (flag === "--show" || flag === "--list" || flag === "--names") {
|
|
@@ -1106,7 +1540,7 @@ async function dx(args) {
|
|
|
1106
1540
|
return await hidden(flag, args.slice(1), services) ?? 0;
|
|
1107
1541
|
}
|
|
1108
1542
|
const emit = flag === "--emit";
|
|
1109
|
-
const parsed = parse(emit ? args.slice(1) : args, usage2, options, { replaces: "docker compose exec <service> bash, logs -f and restart typed per service", examples: ["dx", ["dx php", "a shell in php, or php brought up when stopped"], ["dx php logs", "follow its logs"], ["dx -", "the last action again"]] });
|
|
1543
|
+
const parsed = parse(emit ? args.slice(1) : args, usage2, options, { allowNegative: true, exits: EXITS, replaces: "docker compose exec <service> bash, logs -f and restart typed per service", examples: ["dx", ["dx php", "a shell in php, or php brought up when stopped"], ["dx php logs", "follow its logs"], ["dx php logs --tail 50 --no-follow", "the last 50 lines, then exit"], ["dx -", "the last action again"]] });
|
|
1110
1544
|
const [named, asked] = parsed.positionals;
|
|
1111
1545
|
const services = await allServices();
|
|
1112
1546
|
if (services.length === 0)
|
|
@@ -1144,15 +1578,18 @@ async function dx(args) {
|
|
|
1144
1578
|
action = asked;
|
|
1145
1579
|
}
|
|
1146
1580
|
const chosen = action ?? defaultAction(service);
|
|
1147
|
-
const
|
|
1581
|
+
const { tail, follow } = parsed.values;
|
|
1582
|
+
if ((tail !== undefined || follow !== undefined) && chosen !== "logs")
|
|
1583
|
+
fail("--tail and --no-follow apply to the logs action: dx <service> logs");
|
|
1584
|
+
const command = await commandFor(service, chosen, { tail: tail === undefined ? undefined : wholeNumber(tail, "tail", LOGS_TAIL, 0), follow });
|
|
1148
1585
|
record("dx", { id: key(service), args: [chosen] });
|
|
1149
1586
|
if (emit) {
|
|
1150
|
-
console.log(["run", action === undefined ? key(service) : `${key(service)} ${action}`, "", ...command.map((word) => `=${word}`)].join(`
|
|
1587
|
+
console.log(["run", action === undefined ? key(service) : `${key(service)} ${action}`, "", ...(chosen === "logs" ? maskedLogs(command) : command).map((word) => `=${word}`)].join(`
|
|
1151
1588
|
`));
|
|
1152
1589
|
return 0;
|
|
1153
1590
|
}
|
|
1154
|
-
return Bun.spawn(command, { stdio: ["inherit", "inherit", "inherit"] }).exited;
|
|
1591
|
+
return chosen === "logs" ? followMasked(command) : Bun.spawn(command, { stdio: ["inherit", "inherit", "inherit"] }).exited;
|
|
1155
1592
|
}
|
|
1156
1593
|
|
|
1157
1594
|
// src/dx-cli.ts
|
|
1158
|
-
process11.exitCode = await runMain({ name: "dx",
|
|
1595
|
+
process11.exitCode = await runMain({ name: "dx", run: dx });
|