@kaynooo/cli-docker 0.4.0 → 0.5.1

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/sf-cli.js CHANGED
@@ -1,9 +1,8 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env -S bun --no-env-file
2
2
  // @bun
3
3
 
4
4
  // src/sf-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,29 +779,45 @@ 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/sf.ts
754
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
812
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
755
813
  import { homedir as homedir3 } from "os";
756
- import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
814
+ import { dirname as dirname6, join as join6, resolve as resolve2 } from "path";
757
815
  import process10 from "process";
758
816
 
759
817
  // ../kit/src/fzf.ts
760
- import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
818
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
761
819
  import { homedir as homedir2 } from "os";
762
- import { dirname as dirname2, join as join2 } from "path";
820
+ import { dirname as dirname3, join as join3 } from "path";
763
821
  import process8 from "process";
764
822
  var REMEMBERED = 20;
765
823
  function table(rows) {
@@ -815,12 +873,12 @@ function heightFor(count) {
815
873
  return String(Math.max(15, Math.ceil((count + 4) / 0.6)));
816
874
  }
817
875
  function historyFile(name) {
818
- return join2(process8.env.XDG_CACHE_HOME ?? join2(homedir2(), ".cache"), name, "history.json");
876
+ return join3(process8.env.XDG_CACHE_HOME ?? join3(homedir2(), ".cache"), name, "history.json");
819
877
  }
820
878
  function history(name) {
821
879
  try {
822
880
  const file = historyFile(name);
823
- return existsSync(file) ? JSON.parse(readFileSync(file, "utf-8")) : {};
881
+ return existsSync(file) ? JSON.parse(readFileSync2(file, "utf-8")) : {};
824
882
  } catch {
825
883
  return {};
826
884
  }
@@ -832,7 +890,7 @@ function record(name, run) {
832
890
  const all = history(name);
833
891
  const file = historyFile(name);
834
892
  all[process8.cwd()] = [run, ...(all[process8.cwd()] ?? []).filter((earlier) => earlier.id !== run.id)].slice(0, REMEMBERED);
835
- mkdirSync2(dirname2(file), { recursive: true });
893
+ mkdirSync2(dirname3(file), { recursive: true });
836
894
  writeFileSync(file, JSON.stringify(all));
837
895
  }
838
896
  function byRecency(items, previous, idOf) {
@@ -945,25 +1003,377 @@ async function ask(question, { hint, terminal = process9 }, render, press) {
945
1003
  }
946
1004
 
947
1005
  // src/stack.ts
948
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
949
- import { dirname as dirname3, join as join3, relative, resolve } from "path";
1006
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
1007
+ import { dirname as dirname5, join as join5, relative, resolve } from "path";
1008
+
1009
+ // ../env/src/files.ts
1010
+ import { chmod, chown, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
1011
+ import { basename, dirname as dirname4, join as join4 } from "path";
1012
+
1013
+ // ../env/src/dotenv.ts
1014
+ var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
1015
+ var INLINE_COMMENT = /\s+#.*$/;
1016
+ var CARRIAGE_RETURN = /\r$/;
1017
+ var ESCAPES = { n: `
1018
+ `, r: "\r", t: "\t", '"': '"', "\\": "\\", $: "$" };
1019
+ function closingQuote(text, quote) {
1020
+ for (let index = 0;index < text.length; index++) {
1021
+ if (quote === '"' && text[index] === "\\")
1022
+ index++;
1023
+ else if (text[index] === quote)
1024
+ return index;
1025
+ }
1026
+ return -1;
1027
+ }
1028
+ function unescapeDollar(text) {
1029
+ return text.replaceAll("\\$", "$");
1030
+ }
1031
+ function unescapeDouble(text) {
1032
+ let out = "";
1033
+ for (let index = 0;index < text.length; index++) {
1034
+ const next = text[index + 1];
1035
+ if (text[index] === "\\" && next !== undefined && next in ESCAPES) {
1036
+ out += ESCAPES[next];
1037
+ index++;
1038
+ } else {
1039
+ out += text[index];
1040
+ }
1041
+ }
1042
+ return out;
1043
+ }
1044
+ function parseDotenv(text) {
1045
+ const physical = text.split(`
1046
+ `);
1047
+ const lines = [];
1048
+ for (let index = 0;index < physical.length; index++) {
1049
+ const line = physical[index] ?? "";
1050
+ const match = ASSIGNMENT.exec(line.replace(CARRIAGE_RETURN, ""));
1051
+ if (match === null) {
1052
+ lines.push({ raw: line });
1053
+ continue;
1054
+ }
1055
+ const [, key = "", rest = ""] = match;
1056
+ const start = rest.trimStart();
1057
+ const quote = start[0];
1058
+ if (quote !== '"' && quote !== "'" && quote !== "`") {
1059
+ lines.push({ raw: line, key, value: unescapeDollar(start.replace(INLINE_COMMENT, "").trim()) });
1060
+ continue;
1061
+ }
1062
+ let body = start.slice(1);
1063
+ let raw = line;
1064
+ let end = closingQuote(body, quote);
1065
+ while (end === -1 && index + 1 < physical.length) {
1066
+ index++;
1067
+ body += `
1068
+ ${(physical[index] ?? "").replace(CARRIAGE_RETURN, "")}`;
1069
+ raw += `
1070
+ ${physical[index] ?? ""}`;
1071
+ end = closingQuote(body, quote);
1072
+ }
1073
+ const inner = end === -1 ? body : body.slice(0, end);
1074
+ lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : unescapeDollar(inner) });
1075
+ }
1076
+ return { lines };
1077
+ }
1078
+ function entries(file) {
1079
+ const map = new Map;
1080
+ for (const line of file.lines) {
1081
+ if (line.key !== undefined)
1082
+ map.set(line.key, line.value ?? "");
1083
+ }
1084
+ return map;
1085
+ }
1086
+
1087
+ // ../env/src/mask.ts
1088
+ var MIN_KNOWN_LENGTH = 4;
1089
+ var KEY_MASK = "*** private key redacted ***";
1090
+ var PLAIN_VALUE = /^(?:true|false|null|none|nil|yes|no|on|off|undefined)$/i;
1091
+ var PLACEHOLDER = /^[$%{<(*/]/;
1092
+ var URL_PASSWORD = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]*:([^\s'"]{1,1024})@/dgi;
1093
+ var PORT = /^\d{1,5}$/;
1094
+ var ESCAPE2 = /[.*+?^${}()|[\]\\]/g;
1095
+ function isPlaceholder(value) {
1096
+ return PLACEHOLDER.test(value) || PLAIN_VALUE.test(value);
1097
+ }
1098
+ var TOKEN_FORMATS = [
1099
+ { kind: "GitHub token", pattern: /\b(?:gh[pousr]_|github_pat_)\w{20,}/dg },
1100
+ { kind: "GitLab token", pattern: /\bglpat-[\w-]{20,}/dg },
1101
+ { kind: "API key", pattern: /\bsk-[\w-]{20,}/dg },
1102
+ { kind: "Slack token", pattern: /\bx(?:ox[abprs]|app)-[\w-]{10,}/dg },
1103
+ { kind: "JWT", pattern: /\beyJ[\w-]+\.[\w-]+\.[\w-]+/dg },
1104
+ { kind: "AWS access key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/dg },
1105
+ { kind: "npm token", pattern: /\bnpm_[A-Za-z0-9]{30,}/dg },
1106
+ { kind: "Stripe key", pattern: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{10,}/dg },
1107
+ { kind: "Google API key", pattern: /\bAIza[\w-]{35}/dg },
1108
+ { kind: "SendGrid key", pattern: /\bSG\.[\w-]{16,}\.[\w-]{16,}/dg },
1109
+ { kind: "Authorization header", pattern: /\bAuthorization["']?\s*[:=]\s*["']?(?:Bearer|Basic|Token|Digest)\s+([^\s"',;]{8,})/dgi, skip: isPlaceholder },
1110
+ { kind: "Bearer token", pattern: /\bBearer\s+((?=[\w.~+/=-]*\d)[\w.~+/=-]{20,})/dg },
1111
+ { kind: "password", pattern: /(?<![\w-])(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?=([^\s"'&;,]{4,})/dgi, skip: isPlaceholder },
1112
+ { kind: "password", pattern: /(?<![\w-])["']?(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?["']?\s*[:=]\s*(["'])((?:\\.|(?!\1)[^\\\n]){4,})\1/dgi, skip: isPlaceholder }
1113
+ ];
1114
+ var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
1115
+ var PEM_END = /-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
1116
+ var PEM_BODY = /(?<=-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)(?:(?!-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)[\s\S])+/dg;
1117
+ var PEM_BEGIN_ALL = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/g;
1118
+ var URL_PARTS = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
1119
+ var PATH_SEGMENT = /[^/]+/g;
1120
+ var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
1121
+ var WEBHOOK_FORMATS = [
1122
+ ["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
1123
+ ["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
1124
+ ["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
1125
+ ["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
1126
+ ];
1127
+ var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
1128
+ function hint(value) {
1129
+ const shown = Math.min(3, Math.floor(value.length / 4));
1130
+ return `${value.slice(0, shown)}***(${value.length})`;
1131
+ }
1132
+ function forms(value) {
1133
+ return [...new Set([value, encodeURIComponent(value), JSON.stringify(value).slice(1, -1)])];
1134
+ }
1135
+ function knownSecrets(pairs) {
1136
+ const keys = new Map;
1137
+ for (const [key, value] of pairs) {
1138
+ if (value.length < MIN_KNOWN_LENGTH || PLAIN_VALUE.test(value))
1139
+ continue;
1140
+ for (const form of forms(value)) {
1141
+ if (!keys.has(form))
1142
+ keys.set(form, key);
1143
+ }
1144
+ }
1145
+ if (keys.size === 0)
1146
+ return { pattern: undefined, keys };
1147
+ const alternatives = [...keys.keys()].sort((a, b) => b.length - a.length).map((value) => value.replace(ESCAPE2, "\\$&"));
1148
+ return { pattern: new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "g"), keys };
1149
+ }
1150
+ function passwordSpans(text) {
1151
+ const spans = [];
1152
+ for (const url of text.matchAll(URL_PASSWORD)) {
1153
+ const [start, end] = url.indices?.[1] ?? [];
1154
+ const password = url[1] ?? "";
1155
+ const slash = password.indexOf("/");
1156
+ if (start !== undefined && end !== undefined && !(slash !== -1 && PORT.test(password.slice(0, slash))))
1157
+ spans.push([start, end]);
1158
+ }
1159
+ return spans;
1160
+ }
1161
+ function urlPasswords(text) {
1162
+ return passwordSpans(text).map(([start, end]) => text.slice(start, end));
1163
+ }
1164
+ function urlTokens(line) {
1165
+ const found = [];
1166
+ const add = (kind, start, value) => {
1167
+ found.push({ start, end: start + value.length, kind, mask: hint(value) });
1168
+ };
1169
+ for (const url of line.matchAll(URL_PARTS)) {
1170
+ const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
1171
+ const authorityStart = url.indices?.[1]?.[0];
1172
+ const pathStart = url.indices?.[2]?.[0];
1173
+ const queryStart = url.indices?.[3]?.[0];
1174
+ if (authorityStart === undefined || pathStart === undefined)
1175
+ continue;
1176
+ const hostAt = authority.lastIndexOf("@") + 1;
1177
+ const target = authority.slice(hostAt) + path;
1178
+ for (const [kind, pattern] of WEBHOOK_FORMATS) {
1179
+ const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
1180
+ if (start !== undefined && end !== undefined)
1181
+ add(kind, authorityStart + hostAt + start, target.slice(start, end));
1182
+ }
1183
+ for (const segment of path.matchAll(PATH_SEGMENT)) {
1184
+ if (OPAQUE_SEGMENT.test(segment[0]))
1185
+ add("URL token", pathStart + segment.index, segment[0]);
1186
+ }
1187
+ if (query === undefined || queryStart === undefined)
1188
+ continue;
1189
+ let offset = 0;
1190
+ for (const param of query.split("&")) {
1191
+ const equals = param.indexOf("=");
1192
+ if (equals > 0 && equals < param.length - 1 && SECRET_PARAM.test(param.slice(0, equals)))
1193
+ add("URL token", queryStart + offset + equals + 1, param.slice(equals + 1));
1194
+ offset += param.length + 1;
1195
+ }
1196
+ }
1197
+ return found.filter((match) => match.end > match.start);
1198
+ }
1199
+ function findSecrets(line, known) {
1200
+ const found = [];
1201
+ for (const [start, end] of passwordSpans(line))
1202
+ found.push({ start, end, kind: "URL password", mask: "***" });
1203
+ for (const body of line.matchAll(PEM_BODY)) {
1204
+ const [start, end] = body.indices?.[0] ?? [];
1205
+ if (start !== undefined && end !== undefined)
1206
+ found.push({ start, end, kind: "private key", mask: KEY_MASK });
1207
+ }
1208
+ for (const { kind, pattern, skip } of TOKEN_FORMATS) {
1209
+ for (const token of line.matchAll(pattern)) {
1210
+ const [start, end] = token.indices?.at(-1) ?? [];
1211
+ if (start === undefined || end === undefined)
1212
+ continue;
1213
+ const value = line.slice(start, end);
1214
+ if (!skip?.(value))
1215
+ found.push({ start, end, kind, mask: hint(value) });
1216
+ }
1217
+ }
1218
+ found.push(...urlTokens(line));
1219
+ if (known?.pattern !== undefined) {
1220
+ for (const value of line.matchAll(known.pattern))
1221
+ found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
1222
+ }
1223
+ found.sort((a, b) => a.start - b.start || b.end - a.end);
1224
+ const kept = [];
1225
+ for (const match of found) {
1226
+ if (match.start >= (kept.at(-1)?.end ?? 0))
1227
+ kept.push(match);
1228
+ }
1229
+ return kept;
1230
+ }
1231
+ function openKeyBody(line) {
1232
+ const last = [...line.matchAll(PEM_BEGIN_ALL)].at(-1);
1233
+ if (last === undefined)
1234
+ return;
1235
+ const after = line.slice(last.index + last[0].length);
1236
+ return PEM_END.test(after) ? undefined : after.trim();
1237
+ }
1238
+ function maskLine(line, known) {
1239
+ let out = "";
1240
+ let at = 0;
1241
+ for (const match of findSecrets(line, known)) {
1242
+ out += line.slice(at, match.start) + match.mask;
1243
+ at = match.end;
1244
+ }
1245
+ return out + line.slice(at);
1246
+ }
1247
+
1248
+ // ../env/src/files.ts
1249
+ var ENV_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.envrc)$/;
1250
+ var KNOWN_DEPTH = 3;
1251
+ var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
1252
+ async function isFileEntry(entry, path) {
1253
+ if (entry.isFile())
1254
+ return true;
1255
+ if (!entry.isSymbolicLink())
1256
+ return false;
1257
+ try {
1258
+ return (await stat(path)).isFile();
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ function secretPairs(file) {
1264
+ const pairs = [];
1265
+ for (const [key, value] of entries(file)) {
1266
+ if (isSecretName(key))
1267
+ pairs.push([key, value]);
1268
+ else
1269
+ pairs.push(...urlPasswords(value).map((password) => [key, password]));
1270
+ }
1271
+ return pairs;
1272
+ }
1273
+ async function envFilesBelow(folder, depth = 0) {
1274
+ const found = [];
1275
+ let children;
1276
+ try {
1277
+ children = await readdir(folder, { withFileTypes: true });
1278
+ } catch {
1279
+ return found;
1280
+ }
1281
+ for (const entry of children) {
1282
+ const path = join4(folder, entry.name);
1283
+ if (entry.isDirectory() && depth < KNOWN_DEPTH && !SKIPPED.has(entry.name))
1284
+ found.push(...await envFilesBelow(path, depth + 1));
1285
+ else if (ENV_FILE.test(entry.name) && await isFileEntry(entry, path))
1286
+ found.push(path);
1287
+ }
1288
+ return found;
1289
+ }
1290
+ async function loadKnown(folder = process.cwd(), extra = []) {
1291
+ const pairs = extra.flatMap(secretPairs);
1292
+ for (const path of await envFilesBelow(folder)) {
1293
+ try {
1294
+ pairs.push(...secretPairs(parseDotenv(await Bun.file(path).text())));
1295
+ } catch {}
1296
+ }
1297
+ return knownSecrets(pairs);
1298
+ }
1299
+
1300
+ // ../env/src/redactor.ts
1301
+ var MAX_PENDING = 1 << 18;
1302
+ var OVERLAP = 1 << 13;
1303
+
1304
+ class Redactor {
1305
+ known;
1306
+ pending = "";
1307
+ inKey = false;
1308
+ hidden = false;
1309
+ constructor(known) {
1310
+ this.known = known;
1311
+ }
1312
+ push(chunk) {
1313
+ const end = chunk.lastIndexOf(`
1314
+ `);
1315
+ this.pending += chunk;
1316
+ if (end === -1)
1317
+ return this.pending.length > MAX_PENDING ? this.cut() : "";
1318
+ const split = this.pending.length - chunk.length + end + 1;
1319
+ const complete = this.pending.slice(0, split);
1320
+ this.pending = this.pending.slice(split);
1321
+ return complete.split(`
1322
+ `).slice(0, -1).map((line) => this.line(line)).filter((line) => line !== undefined).map((line) => `${line}
1323
+ `).join("");
1324
+ }
1325
+ cut() {
1326
+ const limit = this.pending.length - OVERLAP;
1327
+ const space = this.pending.lastIndexOf(" ", limit);
1328
+ const at = space > 0 ? space + 1 : limit;
1329
+ const head = this.pending.slice(0, at);
1330
+ this.pending = this.pending.slice(at);
1331
+ return this.line(head) ?? "";
1332
+ }
1333
+ end() {
1334
+ const rest = this.pending;
1335
+ this.pending = "";
1336
+ return rest === "" ? "" : this.line(rest) ?? "";
1337
+ }
1338
+ line(line) {
1339
+ if (this.inKey) {
1340
+ const end = PEM_END.exec(line);
1341
+ if (end === null) {
1342
+ if (this.hidden)
1343
+ return;
1344
+ this.hidden = true;
1345
+ return KEY_MASK;
1346
+ }
1347
+ this.inKey = false;
1348
+ return (end.index > 0 && !this.hidden ? KEY_MASK : "") + this.line(line.slice(end.index));
1349
+ }
1350
+ const body = openKeyBody(line);
1351
+ if (body !== undefined) {
1352
+ this.inKey = true;
1353
+ this.hidden = body !== "";
1354
+ }
1355
+ return maskLine(line, this.known);
1356
+ }
1357
+ }
1358
+
1359
+ // src/stack.ts
950
1360
  var LABEL_FORMAT = ["project", "project.config_files", "project.environment_file", "project.working_dir"].map((label) => `{{.Label "com.docker.compose.${label}"}}`).join("\t");
951
1361
  var COMPOSE_FILE_VAR = /^\s*(?:export\s+)?COMPOSE_FILE\s*=\s*\S/m;
952
1362
  var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
953
- var SKIPPED = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
1363
+ var SKIPPED2 = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
954
1364
  var ACTIONS = ["shell", "logs", "restart", "up", "stop", "build"];
955
1365
  function from(dir, command) {
956
1366
  return dir === "." ? command : ["env", "-C", dir, ...command];
957
1367
  }
958
1368
  function children(dir = ".") {
959
- return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED.has(child.name)).map((child) => child.name).sort();
1369
+ return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED2.has(child.name)).map((child) => child.name).sort();
960
1370
  }
961
1371
  function up(dir) {
962
1372
  const dirs = [];
963
- for (let current = dir;; current = join3(current, "..")) {
1373
+ for (let current = dir;; current = join5(current, "..")) {
964
1374
  dirs.push(current);
965
1375
  const absolute = resolve(current);
966
- if (existsSync2(join3(current, ".git")) || dirname3(absolute) === absolute)
1376
+ if (existsSync2(join5(current, ".git")) || dirname5(absolute) === absolute)
967
1377
  return dirs;
968
1378
  }
969
1379
  }
@@ -971,10 +1381,10 @@ function stackAt(dir) {
971
1381
  return { dir, run: from(dir, ["docker", "compose"]), list: { argv: ["docker", "compose"], cwd: resolve(dir), env: process.env } };
972
1382
  }
973
1383
  function holdsStack(dir) {
974
- if (COMPOSE_FILES.some((name) => existsSync2(join3(dir, name))))
1384
+ if (COMPOSE_FILES.some((name) => existsSync2(join5(dir, name))))
975
1385
  return true;
976
1386
  try {
977
- return COMPOSE_FILE_VAR.test(readFileSync2(join3(dir, ".env"), "utf-8"));
1387
+ return COMPOSE_FILE_VAR.test(readFileSync3(join5(dir, ".env"), "utf-8"));
978
1388
  } catch {
979
1389
  return false;
980
1390
  }
@@ -1022,13 +1432,8 @@ async function recordedNearby() {
1022
1432
  return (await labels()).filter((stack) => dirs.has(stack.workdir));
1023
1433
  }
1024
1434
  async function attempt(stack, args) {
1025
- const proc = Bun.spawn([...stack.list.argv, ...args], { cwd: stack.list.cwd, env: stack.list.env, stdout: "pipe", stderr: "pipe" });
1026
- const [text, stderr, code] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
1027
- if (code === 0)
1028
- return { text, error: undefined };
1029
- const last = stderr.trim().split(`
1030
- `).at(-1) ?? "";
1031
- return { text: "", error: last === "" ? `exit ${code}` : last };
1435
+ const result = await captureOutput([...stack.list.argv, ...args], { cwd: stack.list.cwd, env: stack.list.env });
1436
+ return result.code === 0 ? { text: result.stdout, error: undefined } : { text: "", error: composeError(result) };
1032
1437
  }
1033
1438
  async function output(stack, args) {
1034
1439
  return (await attempt(stack, args)).text;
@@ -1072,13 +1477,14 @@ function key(service) {
1072
1477
  async function shellOf(service) {
1073
1478
  return await output(service.stack, ["exec", "-T", service.name, "sh", "-c", "command -v bash"]) ? "bash" : "sh";
1074
1479
  }
1075
- async function commandFor(service, action) {
1480
+ var LOGS_TAIL = 200;
1481
+ async function commandFor(service, action, logs = {}) {
1076
1482
  const run = service.stack.run;
1077
1483
  switch (action) {
1078
1484
  case "shell":
1079
1485
  return [...run, "exec", service.name, await shellOf(service)];
1080
1486
  case "logs":
1081
- return [...run, "logs", "-f", "--tail=200", service.name];
1487
+ return [...run, "logs", ...logs.follow === false ? [] : ["-f"], `--tail=${logs.tail ?? LOGS_TAIL}`, service.name];
1082
1488
  case "restart":
1083
1489
  return [...run, "restart", service.name];
1084
1490
  case "up":
@@ -1104,20 +1510,24 @@ async function serviceNamed(dir, name, project) {
1104
1510
  const stack = recorded === undefined ? stackAt(dir) : labelledStack(dir, recorded);
1105
1511
  return (await services(stack)).find((candidate) => candidate.name === name);
1106
1512
  }
1513
+ function maskLog(text, known) {
1514
+ const redactor = new Redactor(known);
1515
+ return redactor.push(text) + redactor.end();
1516
+ }
1107
1517
  async function logsOf(stack, name) {
1108
- return output(stack, ["logs", "--tail=30", "--no-log-prefix", name]);
1518
+ return maskLog(await output(stack, ["logs", "--tail=30", "--no-log-prefix", name]), await loadKnown());
1109
1519
  }
1110
1520
 
1111
1521
  // src/sf.ts
1112
1522
  var usage2 = `sf [command] [args...]
1113
1523
  sf -
1114
1524
 
1115
- Picks a Symfony console command with fzf and runs it, asking for each required argument it needs. The commands last run come first, per folder, and the picker opens on the list cached from the last run while the console answers.
1525
+ Without a terminal or without fzf (a script, a pipe, an agent) \`sf\` lists the commands, one per line with its description: that listing is the form to use there, and \`sf --list --json\` gives each with its arguments and options. On a terminal with fzf it picks a Symfony console command instead and runs it, asking for each required argument it needs. The commands last run come first, per folder, and the picker opens on the list cached from the last run while the console answers.
1116
1526
  The console is the nearest bin/console up to the repository root, then those of the child folders (keyed \`api/cache:clear\`). It runs in the php service of the nearest compose stack when there is one, else with the local php; the container's working directory is taken to be the app.
1117
- \`sf -\` runs the last command again with the arguments it ran with, \`sf <command> [args...]\` runs without the picker: a name the list does not hold goes to the console as is, which resolves \`c:c\` to \`cache:clear\` itself. Without fzf or a terminal it lists the commands instead.
1118
- The hidden flags --list, --names, --rows, --show, --record and --emit are for term, tmux and the shell completions.
1119
-
1120
- Exits with the code of the console, 1 when there is no bin/console here.`;
1527
+ \`sf -\` runs the last command again with the arguments it ran with, \`sf <command> [args...]\` runs without the picker: a name the list does not hold goes to the console as is, which resolves \`c:c\` to \`cache:clear\` itself.
1528
+ \`sf --list --json\` (or \`sf --json\`) prints every command as JSON, each with its console folder, description, signature, arguments and options.
1529
+ The hidden flags --list, --names, --rows, --show, --record and --emit are for term, tmux and the shell completions.`;
1530
+ var EXITS = [["the code of the console", "a command it ran"], [0, "it listed the commands"], [1, "no bin/console here, or it listed no command"], [2, "a usage error"]];
1121
1531
  var colors = paint({ isTTY: true });
1122
1532
  var MARKUP = /<(info|comment|question|error)>(.*?)<\/\1>/gs;
1123
1533
  var TAGS = { info: "green", comment: "yellow", question: "cyan", error: "red" };
@@ -1142,16 +1552,16 @@ async function consoleAt(dir) {
1142
1552
  return { dir, run: from(dir, ["php", "bin/console"]), list: { argv: ["php", "bin/console"], cwd: resolve2(dir), env: process10.env } };
1143
1553
  }
1144
1554
  async function consoles() {
1145
- const here = up(".").find((dir) => existsSync3(join4(dir, "bin/console")));
1146
- const dirs = [...here === undefined ? [] : [here], ...children().filter((dir) => existsSync3(join4(dir, "bin/console")))];
1555
+ const here = up(".").find((dir) => existsSync3(join6(dir, "bin/console")));
1556
+ const dirs = [...here === undefined ? [] : [here], ...children().filter((dir) => existsSync3(join6(dir, "bin/console")))];
1147
1557
  return Promise.all([...new Set(dirs)].map(consoleAt));
1148
1558
  }
1149
1559
  function cached(dir) {
1150
- return join4(process10.env.XDG_CACHE_HOME ?? join4(homedir3(), ".cache"), "sf", `${resolve2(dir).replaceAll("/", "%")}.json`);
1560
+ return join6(process10.env.XDG_CACHE_HOME ?? join6(homedir3(), ".cache"), "sf", `${resolve2(dir).replaceAll("/", "%")}.json`);
1151
1561
  }
1152
1562
  function fromCache(dir) {
1153
1563
  try {
1154
- return existsSync3(cached(dir)) ? JSON.parse(readFileSync3(cached(dir), "utf-8")) : undefined;
1564
+ return existsSync3(cached(dir)) ? JSON.parse(readFileSync4(cached(dir), "utf-8")) : undefined;
1155
1565
  } catch {
1156
1566
  return;
1157
1567
  }
@@ -1161,12 +1571,17 @@ async function live(app) {
1161
1571
  const text = await new Response(proc.stdout).text();
1162
1572
  if (await proc.exited !== 0)
1163
1573
  return fromCache(app.dir) ?? [];
1164
- const commands = JSON.parse(text).commands.filter((command) => !command.hidden);
1165
- mkdirSync3(dirname4(cached(app.dir)), { recursive: true });
1574
+ let commands;
1575
+ try {
1576
+ commands = JSON.parse(text).commands.filter((command) => !command.hidden);
1577
+ } catch {
1578
+ return fromCache(app.dir) ?? [];
1579
+ }
1580
+ mkdirSync3(dirname6(cached(app.dir)), { recursive: true });
1166
1581
  writeFileSync2(cached(app.dir), JSON.stringify(commands));
1167
1582
  return commands;
1168
1583
  }
1169
- async function entries(fresh) {
1584
+ async function entries2(fresh) {
1170
1585
  const found = await consoles();
1171
1586
  const lists = await Promise.all(found.map(async (app) => fresh ? live(app) : fromCache(app.dir) ?? live(app)));
1172
1587
  return found.flatMap((app, index) => lists[index].map((command) => ({ app, command })));
@@ -1181,6 +1596,18 @@ function named2(name) {
1181
1596
  function signature(command) {
1182
1597
  return argumentsOf(command).map((argument) => `${argument.is_required ? "<" : "["}${argument.name}${argument.is_array ? "..." : ""}${argument.is_required ? ">" : "]"}`).join(" ");
1183
1598
  }
1599
+ function entryJson(entry) {
1600
+ const { command } = entry;
1601
+ return {
1602
+ name: key2(entry),
1603
+ command: command.name,
1604
+ dir: entry.app.dir,
1605
+ description: command.description,
1606
+ signature: signature(command),
1607
+ arguments: argumentsOf(command).map((argument) => ({ name: argument.name, required: argument.is_required, array: argument.is_array, description: argument.description })),
1608
+ options: optionsOf(command).map((option) => ({ name: option.name, shortcut: option.shortcut, acceptsValue: option.accept_value, description: option.description }))
1609
+ };
1610
+ }
1184
1611
  function rows(found) {
1185
1612
  return byRecency(found, runs("sf"), key2).map((entry) => ({
1186
1613
  hidden: [entry.app.dir, entry.command.name],
@@ -1257,14 +1684,14 @@ async function hidden(flag, rest) {
1257
1684
  return 0;
1258
1685
  }
1259
1686
  if (flag === "--rows") {
1260
- console.log(table(rows(await entries(true))).join(`
1687
+ console.log(table(rows(await entries2(true))).join(`
1261
1688
  `));
1262
1689
  return 0;
1263
1690
  }
1264
1691
  if (flag !== "--list" && flag !== "--names")
1265
1692
  return;
1266
1693
  const refreshed = flag === "--list" && (await consoles()).some((found) => fromCache(found.dir) !== undefined);
1267
- const found = await entries(false);
1694
+ const found = await entries2(false);
1268
1695
  if (flag === "--names") {
1269
1696
  console.log(found.map((entry) => `${key2(entry)} ${entry.command.description}`).join(`
1270
1697
  `));
@@ -1282,6 +1709,11 @@ async function hidden(flag, rest) {
1282
1709
  }
1283
1710
  async function sf(args) {
1284
1711
  const [flag = ""] = args;
1712
+ if (args.length <= 2 && args.includes("--json") && (flag === "--json" || flag === "--list")) {
1713
+ const found = await entries2(false);
1714
+ printJson(byRecency(found, runs("sf"), key2).map(entryJson));
1715
+ return 0;
1716
+ }
1285
1717
  const flagged = await hidden(flag, args.slice(1));
1286
1718
  if (flagged !== undefined)
1287
1719
  return flagged;
@@ -1291,13 +1723,14 @@ async function sf(args) {
1291
1723
  const parsed = parse(last ? [] : words, usage2, {}, {
1292
1724
  positionals: ["[command]"],
1293
1725
  trailing: true,
1726
+ exits: EXITS,
1294
1727
  replaces: "bin/console typed in the php container, with the command names and arguments looked up by hand",
1295
1728
  examples: ["sf", ["sf cache:clear", "run it without the picker"], ["sf c:c --env=prod", "the console resolves abbreviations itself"], ["sf -", "the last command again, with its arguments"]]
1296
1729
  });
1297
1730
  const [command] = parsed.positionals;
1298
1731
  const given = last ? words.slice(1) : parsed.rest ?? [];
1299
1732
  const refreshed = command === undefined && !last && (await consoles()).some((found) => fromCache(found.dir) !== undefined);
1300
- const found = await entries(false);
1733
+ const found = await entries2(false);
1301
1734
  if (last) {
1302
1735
  const previous = runs("sf")[0];
1303
1736
  const entry = previous && found.find((candidate) => key2(candidate) === previous.id);
@@ -1331,9 +1764,9 @@ async function sf(args) {
1331
1764
  preview: `${self} --show {1} {2}`,
1332
1765
  bind: refreshed ? [`load:unbind(load)+reload-sync(${self} --rows)`] : []
1333
1766
  });
1334
- const entry = picked && (await entries(false)).find((candidate) => candidate.app.dir === picked.hidden[0] && candidate.command.name === picked.hidden[1]);
1767
+ const entry = picked && (await entries2(false)).find((candidate) => candidate.app.dir === picked.hidden[0] && candidate.command.name === picked.hidden[1]);
1335
1768
  return entry === undefined ? 1 : execute(emitting, key2(entry), key2(entry), commandFor2(entry, []));
1336
1769
  }
1337
1770
 
1338
1771
  // src/sf-cli.ts
1339
- process11.exitCode = await runMain({ name: "sf", version, run: sf });
1772
+ process11.exitCode = await runMain({ name: "sf", run: sf });