@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/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)) {
@@ -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,26 +779,392 @@ 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
- recordUsage(usageEntry(name, undefined, argv, code));
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/dx.ts
754
812
  import process10 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
+
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
+
756
1164
  // ../kit/src/fzf.ts
757
- import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
1165
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
758
1166
  import { homedir as homedir2 } from "os";
759
- import { dirname as dirname2, join as join2 } from "path";
1167
+ import { dirname as dirname4, join as join4 } from "path";
760
1168
  import process8 from "process";
761
1169
  var REMEMBERED = 20;
762
1170
  function table(rows) {
@@ -812,12 +1220,12 @@ function heightFor(count) {
812
1220
  return String(Math.max(15, Math.ceil((count + 4) / 0.6)));
813
1221
  }
814
1222
  function historyFile(name) {
815
- return join2(process8.env.XDG_CACHE_HOME ?? join2(homedir2(), ".cache"), name, "history.json");
1223
+ return join4(process8.env.XDG_CACHE_HOME ?? join4(homedir2(), ".cache"), name, "history.json");
816
1224
  }
817
1225
  function history(name) {
818
1226
  try {
819
1227
  const file = historyFile(name);
820
- return existsSync(file) ? JSON.parse(readFileSync(file, "utf-8")) : {};
1228
+ return existsSync(file) ? JSON.parse(readFileSync2(file, "utf-8")) : {};
821
1229
  } catch {
822
1230
  return {};
823
1231
  }
@@ -829,7 +1237,7 @@ function record(name, run) {
829
1237
  const all = history(name);
830
1238
  const file = historyFile(name);
831
1239
  all[process8.cwd()] = [run, ...(all[process8.cwd()] ?? []).filter((earlier) => earlier.id !== run.id)].slice(0, REMEMBERED);
832
- mkdirSync2(dirname2(file), { recursive: true });
1240
+ mkdirSync2(dirname4(file), { recursive: true });
833
1241
  writeFileSync(file, JSON.stringify(all));
834
1242
  }
835
1243
  function byRecency(items, previous, idOf) {
@@ -855,25 +1263,25 @@ function list(rows) {
855
1263
  import process9 from "process";
856
1264
 
857
1265
  // src/stack.ts
858
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
859
- import { dirname as dirname3, join as join3, relative, resolve } from "path";
1266
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
1267
+ import { dirname as dirname5, join as join5, relative, resolve } from "path";
860
1268
  var LABEL_FORMAT = ["project", "project.config_files", "project.environment_file", "project.working_dir"].map((label) => `{{.Label "com.docker.compose.${label}"}}`).join("\t");
861
1269
  var COMPOSE_FILE_VAR = /^\s*(?:export\s+)?COMPOSE_FILE\s*=\s*\S/m;
862
1270
  var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
863
- var SKIPPED = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
1271
+ var SKIPPED2 = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
864
1272
  var ACTIONS = ["shell", "logs", "restart", "up", "stop", "build"];
865
1273
  function from(dir, command) {
866
1274
  return dir === "." ? command : ["env", "-C", dir, ...command];
867
1275
  }
868
1276
  function children(dir = ".") {
869
- return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED.has(child.name)).map((child) => child.name).sort();
1277
+ return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED2.has(child.name)).map((child) => child.name).sort();
870
1278
  }
871
1279
  function up(dir) {
872
1280
  const dirs = [];
873
- for (let current = dir;; current = join3(current, "..")) {
1281
+ for (let current = dir;; current = join5(current, "..")) {
874
1282
  dirs.push(current);
875
1283
  const absolute = resolve(current);
876
- if (existsSync2(join3(current, ".git")) || dirname3(absolute) === absolute)
1284
+ if (existsSync2(join5(current, ".git")) || dirname5(absolute) === absolute)
877
1285
  return dirs;
878
1286
  }
879
1287
  }
@@ -881,10 +1289,10 @@ function stackAt(dir) {
881
1289
  return { dir, run: from(dir, ["docker", "compose"]), list: { argv: ["docker", "compose"], cwd: resolve(dir), env: process.env } };
882
1290
  }
883
1291
  function holdsStack(dir) {
884
- if (COMPOSE_FILES.some((name) => existsSync2(join3(dir, name))))
1292
+ if (COMPOSE_FILES.some((name) => existsSync2(join5(dir, name))))
885
1293
  return true;
886
1294
  try {
887
- return COMPOSE_FILE_VAR.test(readFileSync2(join3(dir, ".env"), "utf-8"));
1295
+ return COMPOSE_FILE_VAR.test(readFileSync3(join5(dir, ".env"), "utf-8"));
888
1296
  } catch {
889
1297
  return false;
890
1298
  }
@@ -932,13 +1340,8 @@ async function recordedNearby() {
932
1340
  return (await labels()).filter((stack) => dirs.has(stack.workdir));
933
1341
  }
934
1342
  async function attempt(stack, args) {
935
- const proc = Bun.spawn([...stack.list.argv, ...args], { cwd: stack.list.cwd, env: stack.list.env, stdout: "pipe", stderr: "pipe" });
936
- const [text, stderr, code] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
937
- if (code === 0)
938
- return { text, error: undefined };
939
- const last = stderr.trim().split(`
940
- `).at(-1) ?? "";
941
- return { text: "", error: last === "" ? `exit ${code}` : last };
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) };
942
1345
  }
943
1346
  async function output(stack, args) {
944
1347
  return (await attempt(stack, args)).text;
@@ -982,13 +1385,14 @@ function key(service) {
982
1385
  async function shellOf(service) {
983
1386
  return await output(service.stack, ["exec", "-T", service.name, "sh", "-c", "command -v bash"]) ? "bash" : "sh";
984
1387
  }
985
- async function commandFor(service, action) {
1388
+ var LOGS_TAIL = 200;
1389
+ async function commandFor(service, action, logs = {}) {
986
1390
  const run = service.stack.run;
987
1391
  switch (action) {
988
1392
  case "shell":
989
1393
  return [...run, "exec", service.name, await shellOf(service)];
990
1394
  case "logs":
991
- return [...run, "logs", "-f", "--tail=200", service.name];
1395
+ return [...run, "logs", ...logs.follow === false ? [] : ["-f"], `--tail=${logs.tail ?? LOGS_TAIL}`, service.name];
992
1396
  case "restart":
993
1397
  return [...run, "restart", service.name];
994
1398
  case "up":
@@ -1014,8 +1418,12 @@ async function serviceNamed(dir, name, project) {
1014
1418
  const stack = recorded === undefined ? stackAt(dir) : labelledStack(dir, recorded);
1015
1419
  return (await services(stack)).find((candidate) => candidate.name === name);
1016
1420
  }
1421
+ function maskLog(text, known) {
1422
+ const redactor = new Redactor(known);
1423
+ return redactor.push(text) + redactor.end();
1424
+ }
1017
1425
  async function logsOf(stack, name) {
1018
- 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());
1019
1427
  }
1020
1428
 
1021
1429
  // src/picker.ts
@@ -1048,13 +1456,14 @@ function pickService(services) {
1048
1456
  var usage2 = `dx [service] [action]
1049
1457
  dx -
1050
1458
 
1051
- Without a service, picks one with fzf when it is installed and a terminal is attached: 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. Without fzf or a terminal it lists them instead.
1052
- \`dx -\` does the last action again, \`dx <service> [action]\` acts without the picker: \`shell\` (bash when the container has it), \`logs\` (follows the last 200 lines), \`restart\`, \`up\`, \`stop\` or \`build\` (up with a rebuild).
1053
- 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\`).
1054
-
1055
- 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"]];
1056
1463
  var options = {
1057
- 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" }
1058
1467
  };
1059
1468
  function table2(services) {
1060
1469
  const colors = paint(process10.stdout);
@@ -1108,6 +1517,20 @@ async function hidden(flag, rest, services) {
1108
1517
  }
1109
1518
  return;
1110
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
+ }
1111
1534
  async function dx(args) {
1112
1535
  const [flag = ""] = args;
1113
1536
  if (flag === "--show" || flag === "--list" || flag === "--names") {
@@ -1117,7 +1540,7 @@ async function dx(args) {
1117
1540
  return await hidden(flag, args.slice(1), services) ?? 0;
1118
1541
  }
1119
1542
  const emit = flag === "--emit";
1120
- 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"]] });
1121
1544
  const [named, asked] = parsed.positionals;
1122
1545
  const services = await allServices();
1123
1546
  if (services.length === 0)
@@ -1155,15 +1578,18 @@ async function dx(args) {
1155
1578
  action = asked;
1156
1579
  }
1157
1580
  const chosen = action ?? defaultAction(service);
1158
- const command = await commandFor(service, chosen);
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 });
1159
1585
  record("dx", { id: key(service), args: [chosen] });
1160
1586
  if (emit) {
1161
- 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(`
1162
1588
  `));
1163
1589
  return 0;
1164
1590
  }
1165
- return Bun.spawn(command, { stdio: ["inherit", "inherit", "inherit"] }).exited;
1591
+ return chosen === "logs" ? followMasked(command) : Bun.spawn(command, { stdio: ["inherit", "inherit", "inherit"] }).exited;
1166
1592
  }
1167
1593
 
1168
1594
  // src/dx-cli.ts
1169
- process11.exitCode = await runMain({ name: "dx", version, run: dx });
1595
+ process11.exitCode = await runMain({ name: "dx", run: dx });