@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/sf-cli.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
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)) {
|
|
@@ -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,29 +779,45 @@ 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/sf.ts
|
|
751
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
812
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
752
813
|
import { homedir as homedir3 } from "os";
|
|
753
|
-
import { dirname as
|
|
814
|
+
import { dirname as dirname6, join as join6, resolve as resolve2 } from "path";
|
|
754
815
|
import process10 from "process";
|
|
755
816
|
|
|
756
817
|
// ../kit/src/fzf.ts
|
|
757
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
818
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
758
819
|
import { homedir as homedir2 } from "os";
|
|
759
|
-
import { dirname as
|
|
820
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
760
821
|
import process8 from "process";
|
|
761
822
|
var REMEMBERED = 20;
|
|
762
823
|
function table(rows) {
|
|
@@ -812,12 +873,12 @@ function heightFor(count) {
|
|
|
812
873
|
return String(Math.max(15, Math.ceil((count + 4) / 0.6)));
|
|
813
874
|
}
|
|
814
875
|
function historyFile(name) {
|
|
815
|
-
return
|
|
876
|
+
return join3(process8.env.XDG_CACHE_HOME ?? join3(homedir2(), ".cache"), name, "history.json");
|
|
816
877
|
}
|
|
817
878
|
function history(name) {
|
|
818
879
|
try {
|
|
819
880
|
const file = historyFile(name);
|
|
820
|
-
return existsSync(file) ? JSON.parse(
|
|
881
|
+
return existsSync(file) ? JSON.parse(readFileSync2(file, "utf-8")) : {};
|
|
821
882
|
} catch {
|
|
822
883
|
return {};
|
|
823
884
|
}
|
|
@@ -829,7 +890,7 @@ function record(name, run) {
|
|
|
829
890
|
const all = history(name);
|
|
830
891
|
const file = historyFile(name);
|
|
831
892
|
all[process8.cwd()] = [run, ...(all[process8.cwd()] ?? []).filter((earlier) => earlier.id !== run.id)].slice(0, REMEMBERED);
|
|
832
|
-
mkdirSync2(
|
|
893
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
833
894
|
writeFileSync(file, JSON.stringify(all));
|
|
834
895
|
}
|
|
835
896
|
function byRecency(items, previous, idOf) {
|
|
@@ -942,25 +1003,377 @@ async function ask(question, { hint, terminal = process9 }, render, press) {
|
|
|
942
1003
|
}
|
|
943
1004
|
|
|
944
1005
|
// src/stack.ts
|
|
945
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
946
|
-
import { dirname as
|
|
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
|
|
947
1360
|
var LABEL_FORMAT = ["project", "project.config_files", "project.environment_file", "project.working_dir"].map((label) => `{{.Label "com.docker.compose.${label}"}}`).join("\t");
|
|
948
1361
|
var COMPOSE_FILE_VAR = /^\s*(?:export\s+)?COMPOSE_FILE\s*=\s*\S/m;
|
|
949
1362
|
var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
950
|
-
var
|
|
1363
|
+
var SKIPPED2 = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
|
|
951
1364
|
var ACTIONS = ["shell", "logs", "restart", "up", "stop", "build"];
|
|
952
1365
|
function from(dir, command) {
|
|
953
1366
|
return dir === "." ? command : ["env", "-C", dir, ...command];
|
|
954
1367
|
}
|
|
955
1368
|
function children(dir = ".") {
|
|
956
|
-
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !
|
|
1369
|
+
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED2.has(child.name)).map((child) => child.name).sort();
|
|
957
1370
|
}
|
|
958
1371
|
function up(dir) {
|
|
959
1372
|
const dirs = [];
|
|
960
|
-
for (let current = dir;; current =
|
|
1373
|
+
for (let current = dir;; current = join5(current, "..")) {
|
|
961
1374
|
dirs.push(current);
|
|
962
1375
|
const absolute = resolve(current);
|
|
963
|
-
if (existsSync2(
|
|
1376
|
+
if (existsSync2(join5(current, ".git")) || dirname5(absolute) === absolute)
|
|
964
1377
|
return dirs;
|
|
965
1378
|
}
|
|
966
1379
|
}
|
|
@@ -968,10 +1381,10 @@ function stackAt(dir) {
|
|
|
968
1381
|
return { dir, run: from(dir, ["docker", "compose"]), list: { argv: ["docker", "compose"], cwd: resolve(dir), env: process.env } };
|
|
969
1382
|
}
|
|
970
1383
|
function holdsStack(dir) {
|
|
971
|
-
if (COMPOSE_FILES.some((name) => existsSync2(
|
|
1384
|
+
if (COMPOSE_FILES.some((name) => existsSync2(join5(dir, name))))
|
|
972
1385
|
return true;
|
|
973
1386
|
try {
|
|
974
|
-
return COMPOSE_FILE_VAR.test(
|
|
1387
|
+
return COMPOSE_FILE_VAR.test(readFileSync3(join5(dir, ".env"), "utf-8"));
|
|
975
1388
|
} catch {
|
|
976
1389
|
return false;
|
|
977
1390
|
}
|
|
@@ -1018,9 +1431,12 @@ async function recordedNearby() {
|
|
|
1018
1431
|
const dirs = new Set([...up("."), ...children()].map((dir) => resolve(dir)));
|
|
1019
1432
|
return (await labels()).filter((stack) => dirs.has(stack.workdir));
|
|
1020
1433
|
}
|
|
1434
|
+
async function attempt(stack, args) {
|
|
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) };
|
|
1437
|
+
}
|
|
1021
1438
|
async function output(stack, args) {
|
|
1022
|
-
|
|
1023
|
-
return await proc.exited === 0 ? new Response(proc.stdout).text() : "";
|
|
1439
|
+
return (await attempt(stack, args)).text;
|
|
1024
1440
|
}
|
|
1025
1441
|
function containers(text) {
|
|
1026
1442
|
const trimmed = text.trim();
|
|
@@ -1061,13 +1477,14 @@ function key(service) {
|
|
|
1061
1477
|
async function shellOf(service) {
|
|
1062
1478
|
return await output(service.stack, ["exec", "-T", service.name, "sh", "-c", "command -v bash"]) ? "bash" : "sh";
|
|
1063
1479
|
}
|
|
1064
|
-
|
|
1480
|
+
var LOGS_TAIL = 200;
|
|
1481
|
+
async function commandFor(service, action, logs = {}) {
|
|
1065
1482
|
const run = service.stack.run;
|
|
1066
1483
|
switch (action) {
|
|
1067
1484
|
case "shell":
|
|
1068
1485
|
return [...run, "exec", service.name, await shellOf(service)];
|
|
1069
1486
|
case "logs":
|
|
1070
|
-
return [...run, "logs", "-f",
|
|
1487
|
+
return [...run, "logs", ...logs.follow === false ? [] : ["-f"], `--tail=${logs.tail ?? LOGS_TAIL}`, service.name];
|
|
1071
1488
|
case "restart":
|
|
1072
1489
|
return [...run, "restart", service.name];
|
|
1073
1490
|
case "up":
|
|
@@ -1093,20 +1510,24 @@ async function serviceNamed(dir, name, project) {
|
|
|
1093
1510
|
const stack = recorded === undefined ? stackAt(dir) : labelledStack(dir, recorded);
|
|
1094
1511
|
return (await services(stack)).find((candidate) => candidate.name === name);
|
|
1095
1512
|
}
|
|
1513
|
+
function maskLog(text, known) {
|
|
1514
|
+
const redactor = new Redactor(known);
|
|
1515
|
+
return redactor.push(text) + redactor.end();
|
|
1516
|
+
}
|
|
1096
1517
|
async function logsOf(stack, name) {
|
|
1097
|
-
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());
|
|
1098
1519
|
}
|
|
1099
1520
|
|
|
1100
1521
|
// src/sf.ts
|
|
1101
1522
|
var usage2 = `sf [command] [args...]
|
|
1102
1523
|
sf -
|
|
1103
1524
|
|
|
1104
|
-
|
|
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.
|
|
1105
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.
|
|
1106
|
-
\`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.
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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"]];
|
|
1110
1531
|
var colors = paint({ isTTY: true });
|
|
1111
1532
|
var MARKUP = /<(info|comment|question|error)>(.*?)<\/\1>/gs;
|
|
1112
1533
|
var TAGS = { info: "green", comment: "yellow", question: "cyan", error: "red" };
|
|
@@ -1131,16 +1552,16 @@ async function consoleAt(dir) {
|
|
|
1131
1552
|
return { dir, run: from(dir, ["php", "bin/console"]), list: { argv: ["php", "bin/console"], cwd: resolve2(dir), env: process10.env } };
|
|
1132
1553
|
}
|
|
1133
1554
|
async function consoles() {
|
|
1134
|
-
const here = up(".").find((dir) => existsSync3(
|
|
1135
|
-
const dirs = [...here === undefined ? [] : [here], ...children().filter((dir) => existsSync3(
|
|
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")))];
|
|
1136
1557
|
return Promise.all([...new Set(dirs)].map(consoleAt));
|
|
1137
1558
|
}
|
|
1138
1559
|
function cached(dir) {
|
|
1139
|
-
return
|
|
1560
|
+
return join6(process10.env.XDG_CACHE_HOME ?? join6(homedir3(), ".cache"), "sf", `${resolve2(dir).replaceAll("/", "%")}.json`);
|
|
1140
1561
|
}
|
|
1141
1562
|
function fromCache(dir) {
|
|
1142
1563
|
try {
|
|
1143
|
-
return existsSync3(cached(dir)) ? JSON.parse(
|
|
1564
|
+
return existsSync3(cached(dir)) ? JSON.parse(readFileSync4(cached(dir), "utf-8")) : undefined;
|
|
1144
1565
|
} catch {
|
|
1145
1566
|
return;
|
|
1146
1567
|
}
|
|
@@ -1150,12 +1571,17 @@ async function live(app) {
|
|
|
1150
1571
|
const text = await new Response(proc.stdout).text();
|
|
1151
1572
|
if (await proc.exited !== 0)
|
|
1152
1573
|
return fromCache(app.dir) ?? [];
|
|
1153
|
-
|
|
1154
|
-
|
|
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 });
|
|
1155
1581
|
writeFileSync2(cached(app.dir), JSON.stringify(commands));
|
|
1156
1582
|
return commands;
|
|
1157
1583
|
}
|
|
1158
|
-
async function
|
|
1584
|
+
async function entries2(fresh) {
|
|
1159
1585
|
const found = await consoles();
|
|
1160
1586
|
const lists = await Promise.all(found.map(async (app) => fresh ? live(app) : fromCache(app.dir) ?? live(app)));
|
|
1161
1587
|
return found.flatMap((app, index) => lists[index].map((command) => ({ app, command })));
|
|
@@ -1170,6 +1596,18 @@ function named2(name) {
|
|
|
1170
1596
|
function signature(command) {
|
|
1171
1597
|
return argumentsOf(command).map((argument) => `${argument.is_required ? "<" : "["}${argument.name}${argument.is_array ? "..." : ""}${argument.is_required ? ">" : "]"}`).join(" ");
|
|
1172
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
|
+
}
|
|
1173
1611
|
function rows(found) {
|
|
1174
1612
|
return byRecency(found, runs("sf"), key2).map((entry) => ({
|
|
1175
1613
|
hidden: [entry.app.dir, entry.command.name],
|
|
@@ -1246,14 +1684,14 @@ async function hidden(flag, rest) {
|
|
|
1246
1684
|
return 0;
|
|
1247
1685
|
}
|
|
1248
1686
|
if (flag === "--rows") {
|
|
1249
|
-
console.log(table(rows(await
|
|
1687
|
+
console.log(table(rows(await entries2(true))).join(`
|
|
1250
1688
|
`));
|
|
1251
1689
|
return 0;
|
|
1252
1690
|
}
|
|
1253
1691
|
if (flag !== "--list" && flag !== "--names")
|
|
1254
1692
|
return;
|
|
1255
1693
|
const refreshed = flag === "--list" && (await consoles()).some((found) => fromCache(found.dir) !== undefined);
|
|
1256
|
-
const found = await
|
|
1694
|
+
const found = await entries2(false);
|
|
1257
1695
|
if (flag === "--names") {
|
|
1258
1696
|
console.log(found.map((entry) => `${key2(entry)} ${entry.command.description}`).join(`
|
|
1259
1697
|
`));
|
|
@@ -1271,6 +1709,11 @@ async function hidden(flag, rest) {
|
|
|
1271
1709
|
}
|
|
1272
1710
|
async function sf(args) {
|
|
1273
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
|
+
}
|
|
1274
1717
|
const flagged = await hidden(flag, args.slice(1));
|
|
1275
1718
|
if (flagged !== undefined)
|
|
1276
1719
|
return flagged;
|
|
@@ -1280,13 +1723,14 @@ async function sf(args) {
|
|
|
1280
1723
|
const parsed = parse(last ? [] : words, usage2, {}, {
|
|
1281
1724
|
positionals: ["[command]"],
|
|
1282
1725
|
trailing: true,
|
|
1726
|
+
exits: EXITS,
|
|
1283
1727
|
replaces: "bin/console typed in the php container, with the command names and arguments looked up by hand",
|
|
1284
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"]]
|
|
1285
1729
|
});
|
|
1286
1730
|
const [command] = parsed.positionals;
|
|
1287
1731
|
const given = last ? words.slice(1) : parsed.rest ?? [];
|
|
1288
1732
|
const refreshed = command === undefined && !last && (await consoles()).some((found) => fromCache(found.dir) !== undefined);
|
|
1289
|
-
const found = await
|
|
1733
|
+
const found = await entries2(false);
|
|
1290
1734
|
if (last) {
|
|
1291
1735
|
const previous = runs("sf")[0];
|
|
1292
1736
|
const entry = previous && found.find((candidate) => key2(candidate) === previous.id);
|
|
@@ -1320,9 +1764,9 @@ async function sf(args) {
|
|
|
1320
1764
|
preview: `${self} --show {1} {2}`,
|
|
1321
1765
|
bind: refreshed ? [`load:unbind(load)+reload-sync(${self} --rows)`] : []
|
|
1322
1766
|
});
|
|
1323
|
-
const entry = picked && (await
|
|
1767
|
+
const entry = picked && (await entries2(false)).find((candidate) => candidate.app.dir === picked.hidden[0] && candidate.command.name === picked.hidden[1]);
|
|
1324
1768
|
return entry === undefined ? 1 : execute(emitting, key2(entry), key2(entry), commandFor2(entry, []));
|
|
1325
1769
|
}
|
|
1326
1770
|
|
|
1327
1771
|
// src/sf-cli.ts
|
|
1328
|
-
process11.exitCode = await runMain({ name: "sf",
|
|
1772
|
+
process11.exitCode = await runMain({ name: "sf", run: sf });
|