@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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# docker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Four helpers: `dc` runs compose on the stack found here, `dx` lists a stack's services and opens a shell in one, follows its logs, restarts, stops or rebuilds it, `dlog` shows every service's health and recent errors, `sf` picks a Symfony console command and runs it.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
bun install -g @kaynooo/cli-docker
|
|
@@ -12,10 +12,13 @@ bun install -g @kaynooo/cli-docker
|
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| `dc <compose args>` | `docker compose` on the nearest compose file (or `.env` setting `COMPOSE_FILE`) up to the repository root |
|
|
14
14
|
| `dc <project> <compose args>` | the same on a stack that came up from other files (`-f docker-compose.prod.yaml --env-file .env.prod`), found through the labels compose put on its containers |
|
|
15
|
-
| `dx` |
|
|
15
|
+
| `dx` | without a terminal or fzf (a script, a pipe, an agent) it lists the services with their state, ports and image, `--json` for the full record: that is the form to use there. On a terminal with fzf it picks one instead: enter opens its shell (or brings it up), ctrl-l follows its logs, ctrl-r restarts, ctrl-x stops, ctrl-b rebuilds; the ones acted on last come first |
|
|
16
16
|
| `dx -` | the last action again |
|
|
17
17
|
| `dx <service> [action]` | `shell` (the default, `up` for a stopped service), `logs`, `restart`, `up`, `stop` or `build` |
|
|
18
|
-
| `
|
|
18
|
+
| `dx <service> logs --tail 50 --no-follow` | `--tail N` sets the lines (200), `--no-follow` prints them once and exits instead of following, so a script or an agent gets them without hanging; both are refused for any other action |
|
|
19
|
+
| `dlog [service...]` | one line per service with its state and health, then the last distinct error lines of its logs (`-n` per service, `--since 1h`), a repeated message once with `×N` and secrets masked as `redact` does; `--all` adds the last lines of a quiet service. `--level warn` keeps warnings too and `--grep <regex>` keeps the lines matching a case insensitive regex instead of the error words. Exits 1 when a service is down or unhealthy or logged a line; `--fail-on down|unhealthy|errors` (repeatable, all three by default) picks what counts, so `--fail-on down --fail-on unhealthy` gates on health and ignores an old error still inside `--since` |
|
|
20
|
+
| `sf` | without a terminal or fzf (a script, a pipe, an agent) it lists the Symfony console commands with their description, `sf --list --json` adding arguments and options: that is the form to use there. On a terminal with fzf it picks a command and runs it, asking for each required argument; the commands run last come first, and the picker opens on the list cached from the last run. It runs in the php service of the nearest compose stack, else with the local php |
|
|
21
|
+
| `sf --list --json` | every command with its console folder, signature, arguments and options as JSON |
|
|
19
22
|
| `sf -` | the last command again, with the arguments it ran with |
|
|
20
23
|
| `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 |
|
|
21
24
|
|
package/dist/dc-cli.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
// src/dc-cli.ts
|
|
5
5
|
import process9 from "process";
|
|
6
|
-
|
|
7
6
|
// ../kit/src/args.ts
|
|
8
7
|
import process4 from "process";
|
|
9
8
|
import { parseArgs } from "util";
|
|
@@ -306,17 +305,23 @@ var FULL_STOP = /\.$/;
|
|
|
306
305
|
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
307
306
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
308
307
|
}
|
|
309
|
-
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces } = {}) {
|
|
310
|
-
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exampleRows(examples, colors)].join(`
|
|
308
|
+
function helpText(usage, options, colors = paint(process3.stdout), { examples = [], replaces, exits = [] } = {}) {
|
|
309
|
+
return [usageLine(usage, colors), ...replacesRows(replaces, colors), "", colors.bold("Options:"), ...optionRows(options, colors), ...exitRows(exits, colors), ...exampleRows(examples, colors)].join(`
|
|
311
310
|
`);
|
|
312
311
|
}
|
|
312
|
+
function exitRows(exits, colors = paint(process3.stdout)) {
|
|
313
|
+
if (exits.length === 0)
|
|
314
|
+
return [];
|
|
315
|
+
const width = Math.max(...exits.map(([code]) => String(code).length));
|
|
316
|
+
return ["", colors.bold("Exits:"), ...exits.map(([code, meaning]) => ` ${colors.cyan(String(code).padEnd(width))} ${meaning}`)];
|
|
317
|
+
}
|
|
313
318
|
function replacesRows(replaces, colors = paint(process3.stdout)) {
|
|
314
319
|
return replaces === undefined ? [] : ["", `${colors.bold("Instead of:")} ${replaces}`];
|
|
315
320
|
}
|
|
316
321
|
function wantsHelpJson(env = process3.env) {
|
|
317
322
|
return env.KIT_HELP === "json";
|
|
318
323
|
}
|
|
319
|
-
function description(usage, { examples = [], replaces } = {}) {
|
|
324
|
+
function description(usage, { examples = [], replaces, exits } = {}, options) {
|
|
320
325
|
const lines = usage.replace(USAGE_PREFIX, "").split(`
|
|
321
326
|
`);
|
|
322
327
|
const first = lines[0];
|
|
@@ -327,7 +332,9 @@ function description(usage, { examples = [], replaces } = {}) {
|
|
|
327
332
|
synopsis: colon === -1 ? first : first.slice(0, colon),
|
|
328
333
|
summary: colon === -1 ? fallback : first.slice(colon + 2),
|
|
329
334
|
...replaces === undefined ? {} : { replaces },
|
|
330
|
-
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] })
|
|
335
|
+
examples: examples.map((example) => typeof example === "string" ? { line: example } : { line: example[0], note: example[1] }),
|
|
336
|
+
...exits === undefined ? {} : { exits: exits.map(([code, meaning]) => ({ code, meaning })) },
|
|
337
|
+
...options === undefined ? {} : { options: Object.entries(options).map(([name, { short, description }]) => ({ name, ...short === undefined ? {} : { short }, ...description === undefined ? {} : { description } })) }
|
|
331
338
|
};
|
|
332
339
|
}
|
|
333
340
|
function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
@@ -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,42 +779,410 @@ 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/dc.ts
|
|
751
812
|
import process8 from "process";
|
|
752
813
|
|
|
753
814
|
// src/stack.ts
|
|
754
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
755
|
-
import { dirname as
|
|
815
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
816
|
+
import { dirname as dirname4, join as join4, relative, resolve } from "path";
|
|
817
|
+
|
|
818
|
+
// ../env/src/files.ts
|
|
819
|
+
import { chmod, chown, readdir, realpath, rename, rm, stat, writeFile } from "fs/promises";
|
|
820
|
+
import { basename, dirname as dirname3, join as join3 } from "path";
|
|
821
|
+
|
|
822
|
+
// ../env/src/dotenv.ts
|
|
823
|
+
var ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i;
|
|
824
|
+
var INLINE_COMMENT = /\s+#.*$/;
|
|
825
|
+
var CARRIAGE_RETURN = /\r$/;
|
|
826
|
+
var ESCAPES = { n: `
|
|
827
|
+
`, r: "\r", t: "\t", '"': '"', "\\": "\\", $: "$" };
|
|
828
|
+
function closingQuote(text, quote) {
|
|
829
|
+
for (let index = 0;index < text.length; index++) {
|
|
830
|
+
if (quote === '"' && text[index] === "\\")
|
|
831
|
+
index++;
|
|
832
|
+
else if (text[index] === quote)
|
|
833
|
+
return index;
|
|
834
|
+
}
|
|
835
|
+
return -1;
|
|
836
|
+
}
|
|
837
|
+
function unescapeDollar(text) {
|
|
838
|
+
return text.replaceAll("\\$", "$");
|
|
839
|
+
}
|
|
840
|
+
function unescapeDouble(text) {
|
|
841
|
+
let out = "";
|
|
842
|
+
for (let index = 0;index < text.length; index++) {
|
|
843
|
+
const next = text[index + 1];
|
|
844
|
+
if (text[index] === "\\" && next !== undefined && next in ESCAPES) {
|
|
845
|
+
out += ESCAPES[next];
|
|
846
|
+
index++;
|
|
847
|
+
} else {
|
|
848
|
+
out += text[index];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
853
|
+
function parseDotenv(text) {
|
|
854
|
+
const physical = text.split(`
|
|
855
|
+
`);
|
|
856
|
+
const lines = [];
|
|
857
|
+
for (let index = 0;index < physical.length; index++) {
|
|
858
|
+
const line = physical[index] ?? "";
|
|
859
|
+
const match = ASSIGNMENT.exec(line.replace(CARRIAGE_RETURN, ""));
|
|
860
|
+
if (match === null) {
|
|
861
|
+
lines.push({ raw: line });
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const [, key = "", rest = ""] = match;
|
|
865
|
+
const start = rest.trimStart();
|
|
866
|
+
const quote = start[0];
|
|
867
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") {
|
|
868
|
+
lines.push({ raw: line, key, value: unescapeDollar(start.replace(INLINE_COMMENT, "").trim()) });
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
let body = start.slice(1);
|
|
872
|
+
let raw = line;
|
|
873
|
+
let end = closingQuote(body, quote);
|
|
874
|
+
while (end === -1 && index + 1 < physical.length) {
|
|
875
|
+
index++;
|
|
876
|
+
body += `
|
|
877
|
+
${(physical[index] ?? "").replace(CARRIAGE_RETURN, "")}`;
|
|
878
|
+
raw += `
|
|
879
|
+
${physical[index] ?? ""}`;
|
|
880
|
+
end = closingQuote(body, quote);
|
|
881
|
+
}
|
|
882
|
+
const inner = end === -1 ? body : body.slice(0, end);
|
|
883
|
+
lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : unescapeDollar(inner) });
|
|
884
|
+
}
|
|
885
|
+
return { lines };
|
|
886
|
+
}
|
|
887
|
+
function entries(file) {
|
|
888
|
+
const map = new Map;
|
|
889
|
+
for (const line of file.lines) {
|
|
890
|
+
if (line.key !== undefined)
|
|
891
|
+
map.set(line.key, line.value ?? "");
|
|
892
|
+
}
|
|
893
|
+
return map;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ../env/src/mask.ts
|
|
897
|
+
var MIN_KNOWN_LENGTH = 4;
|
|
898
|
+
var KEY_MASK = "*** private key redacted ***";
|
|
899
|
+
var PLAIN_VALUE = /^(?:true|false|null|none|nil|yes|no|on|off|undefined)$/i;
|
|
900
|
+
var PLACEHOLDER = /^[$%{<(*/]/;
|
|
901
|
+
var URL_PASSWORD = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/@]*:([^\s'"]{1,1024})@/dgi;
|
|
902
|
+
var PORT = /^\d{1,5}$/;
|
|
903
|
+
var ESCAPE2 = /[.*+?^${}()|[\]\\]/g;
|
|
904
|
+
function isPlaceholder(value) {
|
|
905
|
+
return PLACEHOLDER.test(value) || PLAIN_VALUE.test(value);
|
|
906
|
+
}
|
|
907
|
+
var TOKEN_FORMATS = [
|
|
908
|
+
{ kind: "GitHub token", pattern: /\b(?:gh[pousr]_|github_pat_)\w{20,}/dg },
|
|
909
|
+
{ kind: "GitLab token", pattern: /\bglpat-[\w-]{20,}/dg },
|
|
910
|
+
{ kind: "API key", pattern: /\bsk-[\w-]{20,}/dg },
|
|
911
|
+
{ kind: "Slack token", pattern: /\bx(?:ox[abprs]|app)-[\w-]{10,}/dg },
|
|
912
|
+
{ kind: "JWT", pattern: /\beyJ[\w-]+\.[\w-]+\.[\w-]+/dg },
|
|
913
|
+
{ kind: "AWS access key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/dg },
|
|
914
|
+
{ kind: "npm token", pattern: /\bnpm_[A-Za-z0-9]{30,}/dg },
|
|
915
|
+
{ kind: "Stripe key", pattern: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{10,}/dg },
|
|
916
|
+
{ kind: "Google API key", pattern: /\bAIza[\w-]{35}/dg },
|
|
917
|
+
{ kind: "SendGrid key", pattern: /\bSG\.[\w-]{16,}\.[\w-]{16,}/dg },
|
|
918
|
+
{ kind: "Authorization header", pattern: /\bAuthorization["']?\s*[:=]\s*["']?(?:Bearer|Basic|Token|Digest)\s+([^\s"',;]{8,})/dgi, skip: isPlaceholder },
|
|
919
|
+
{ kind: "Bearer token", pattern: /\bBearer\s+((?=[\w.~+/=-]*\d)[\w.~+/=-]{20,})/dg },
|
|
920
|
+
{ kind: "password", pattern: /(?<![\w-])(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?=([^\s"'&;,]{4,})/dgi, skip: isPlaceholder },
|
|
921
|
+
{ kind: "password", pattern: /(?<![\w-])["']?(?:[\w-]{0,30}[_-])?(?:password|passwd|secret|api[_-]?key)(?:[_-][\w-]{0,30})?["']?\s*[:=]\s*(["'])((?:\\.|(?!\1)[^\\\n]){4,})\1/dgi, skip: isPlaceholder }
|
|
922
|
+
];
|
|
923
|
+
var PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
924
|
+
var PEM_END = /-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/;
|
|
925
|
+
var PEM_BODY = /(?<=-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)(?:(?!-----END [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)[\s\S])+/dg;
|
|
926
|
+
var PEM_BEGIN_ALL = /-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
927
|
+
var URL_PARTS = /(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,31}:\/\/([^\s/?#'"<>`]*)([^\s?#'"<>`]*)(?:\?([^\s#'"<>`]*))?/dgi;
|
|
928
|
+
var PATH_SEGMENT = /[^/]+/g;
|
|
929
|
+
var SECRET_PARAM = /^(?:token|[a-z]+_token|key|api[_-]?key|access[_-]?key|secret|[a-z]+_secret|password|passwd|pwd|sig|signature|auth|authorization)$/i;
|
|
930
|
+
var WEBHOOK_FORMATS = [
|
|
931
|
+
["Discord webhook", /(?:^|\.)discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/([\w-]+)/di],
|
|
932
|
+
["Slack webhook", /^hooks\.slack\.com\/(?:services|workflows)\/(?:[\w-]+\/)*([\w-]+)/di],
|
|
933
|
+
["Uptime Kuma push URL", /\/api\/push\/([\w-]+)/di],
|
|
934
|
+
["healthchecks.io ping URL", /^hc-ping\.com\/([\w-]+)/di]
|
|
935
|
+
];
|
|
936
|
+
var OPAQUE_SEGMENT = /^(?:(?=[\w-]{20,}$)(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\w-]+|(?=[A-Za-z\d]{32,}$)(?=.*[A-Za-z])(?=.*\d).+)$/;
|
|
937
|
+
function hint(value) {
|
|
938
|
+
const shown = Math.min(3, Math.floor(value.length / 4));
|
|
939
|
+
return `${value.slice(0, shown)}***(${value.length})`;
|
|
940
|
+
}
|
|
941
|
+
function forms(value) {
|
|
942
|
+
return [...new Set([value, encodeURIComponent(value), JSON.stringify(value).slice(1, -1)])];
|
|
943
|
+
}
|
|
944
|
+
function knownSecrets(pairs) {
|
|
945
|
+
const keys = new Map;
|
|
946
|
+
for (const [key, value] of pairs) {
|
|
947
|
+
if (value.length < MIN_KNOWN_LENGTH || PLAIN_VALUE.test(value))
|
|
948
|
+
continue;
|
|
949
|
+
for (const form of forms(value)) {
|
|
950
|
+
if (!keys.has(form))
|
|
951
|
+
keys.set(form, key);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (keys.size === 0)
|
|
955
|
+
return { pattern: undefined, keys };
|
|
956
|
+
const alternatives = [...keys.keys()].sort((a, b) => b.length - a.length).map((value) => value.replace(ESCAPE2, "\\$&"));
|
|
957
|
+
return { pattern: new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "g"), keys };
|
|
958
|
+
}
|
|
959
|
+
function passwordSpans(text) {
|
|
960
|
+
const spans = [];
|
|
961
|
+
for (const url of text.matchAll(URL_PASSWORD)) {
|
|
962
|
+
const [start, end] = url.indices?.[1] ?? [];
|
|
963
|
+
const password = url[1] ?? "";
|
|
964
|
+
const slash = password.indexOf("/");
|
|
965
|
+
if (start !== undefined && end !== undefined && !(slash !== -1 && PORT.test(password.slice(0, slash))))
|
|
966
|
+
spans.push([start, end]);
|
|
967
|
+
}
|
|
968
|
+
return spans;
|
|
969
|
+
}
|
|
970
|
+
function urlPasswords(text) {
|
|
971
|
+
return passwordSpans(text).map(([start, end]) => text.slice(start, end));
|
|
972
|
+
}
|
|
973
|
+
function urlTokens(line) {
|
|
974
|
+
const found = [];
|
|
975
|
+
const add = (kind, start, value) => {
|
|
976
|
+
found.push({ start, end: start + value.length, kind, mask: hint(value) });
|
|
977
|
+
};
|
|
978
|
+
for (const url of line.matchAll(URL_PARTS)) {
|
|
979
|
+
const [authority, path = "", query] = [url[1] ?? "", url[2] ?? "", url[3]];
|
|
980
|
+
const authorityStart = url.indices?.[1]?.[0];
|
|
981
|
+
const pathStart = url.indices?.[2]?.[0];
|
|
982
|
+
const queryStart = url.indices?.[3]?.[0];
|
|
983
|
+
if (authorityStart === undefined || pathStart === undefined)
|
|
984
|
+
continue;
|
|
985
|
+
const hostAt = authority.lastIndexOf("@") + 1;
|
|
986
|
+
const target = authority.slice(hostAt) + path;
|
|
987
|
+
for (const [kind, pattern] of WEBHOOK_FORMATS) {
|
|
988
|
+
const [start, end] = pattern.exec(target)?.indices?.[1] ?? [];
|
|
989
|
+
if (start !== undefined && end !== undefined)
|
|
990
|
+
add(kind, authorityStart + hostAt + start, target.slice(start, end));
|
|
991
|
+
}
|
|
992
|
+
for (const segment of path.matchAll(PATH_SEGMENT)) {
|
|
993
|
+
if (OPAQUE_SEGMENT.test(segment[0]))
|
|
994
|
+
add("URL token", pathStart + segment.index, segment[0]);
|
|
995
|
+
}
|
|
996
|
+
if (query === undefined || queryStart === undefined)
|
|
997
|
+
continue;
|
|
998
|
+
let offset = 0;
|
|
999
|
+
for (const param of query.split("&")) {
|
|
1000
|
+
const equals = param.indexOf("=");
|
|
1001
|
+
if (equals > 0 && equals < param.length - 1 && SECRET_PARAM.test(param.slice(0, equals)))
|
|
1002
|
+
add("URL token", queryStart + offset + equals + 1, param.slice(equals + 1));
|
|
1003
|
+
offset += param.length + 1;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return found.filter((match) => match.end > match.start);
|
|
1007
|
+
}
|
|
1008
|
+
function findSecrets(line, known) {
|
|
1009
|
+
const found = [];
|
|
1010
|
+
for (const [start, end] of passwordSpans(line))
|
|
1011
|
+
found.push({ start, end, kind: "URL password", mask: "***" });
|
|
1012
|
+
for (const body of line.matchAll(PEM_BODY)) {
|
|
1013
|
+
const [start, end] = body.indices?.[0] ?? [];
|
|
1014
|
+
if (start !== undefined && end !== undefined)
|
|
1015
|
+
found.push({ start, end, kind: "private key", mask: KEY_MASK });
|
|
1016
|
+
}
|
|
1017
|
+
for (const { kind, pattern, skip } of TOKEN_FORMATS) {
|
|
1018
|
+
for (const token of line.matchAll(pattern)) {
|
|
1019
|
+
const [start, end] = token.indices?.at(-1) ?? [];
|
|
1020
|
+
if (start === undefined || end === undefined)
|
|
1021
|
+
continue;
|
|
1022
|
+
const value = line.slice(start, end);
|
|
1023
|
+
if (!skip?.(value))
|
|
1024
|
+
found.push({ start, end, kind, mask: hint(value) });
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
found.push(...urlTokens(line));
|
|
1028
|
+
if (known?.pattern !== undefined) {
|
|
1029
|
+
for (const value of line.matchAll(known.pattern))
|
|
1030
|
+
found.push({ start: value.index, end: value.index + value[0].length, kind: `value of ${known.keys.get(value[0])}`, mask: hint(value[0]) });
|
|
1031
|
+
}
|
|
1032
|
+
found.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1033
|
+
const kept = [];
|
|
1034
|
+
for (const match of found) {
|
|
1035
|
+
if (match.start >= (kept.at(-1)?.end ?? 0))
|
|
1036
|
+
kept.push(match);
|
|
1037
|
+
}
|
|
1038
|
+
return kept;
|
|
1039
|
+
}
|
|
1040
|
+
function openKeyBody(line) {
|
|
1041
|
+
const last = [...line.matchAll(PEM_BEGIN_ALL)].at(-1);
|
|
1042
|
+
if (last === undefined)
|
|
1043
|
+
return;
|
|
1044
|
+
const after = line.slice(last.index + last[0].length);
|
|
1045
|
+
return PEM_END.test(after) ? undefined : after.trim();
|
|
1046
|
+
}
|
|
1047
|
+
function maskLine(line, known) {
|
|
1048
|
+
let out = "";
|
|
1049
|
+
let at = 0;
|
|
1050
|
+
for (const match of findSecrets(line, known)) {
|
|
1051
|
+
out += line.slice(at, match.start) + match.mask;
|
|
1052
|
+
at = match.end;
|
|
1053
|
+
}
|
|
1054
|
+
return out + line.slice(at);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ../env/src/files.ts
|
|
1058
|
+
var ENV_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.envrc)$/;
|
|
1059
|
+
var KNOWN_DEPTH = 3;
|
|
1060
|
+
var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
|
|
1061
|
+
async function isFileEntry(entry, path) {
|
|
1062
|
+
if (entry.isFile())
|
|
1063
|
+
return true;
|
|
1064
|
+
if (!entry.isSymbolicLink())
|
|
1065
|
+
return false;
|
|
1066
|
+
try {
|
|
1067
|
+
return (await stat(path)).isFile();
|
|
1068
|
+
} catch {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function secretPairs(file) {
|
|
1073
|
+
const pairs = [];
|
|
1074
|
+
for (const [key, value] of entries(file)) {
|
|
1075
|
+
if (isSecretName(key))
|
|
1076
|
+
pairs.push([key, value]);
|
|
1077
|
+
else
|
|
1078
|
+
pairs.push(...urlPasswords(value).map((password) => [key, password]));
|
|
1079
|
+
}
|
|
1080
|
+
return pairs;
|
|
1081
|
+
}
|
|
1082
|
+
async function envFilesBelow(folder, depth = 0) {
|
|
1083
|
+
const found = [];
|
|
1084
|
+
let children;
|
|
1085
|
+
try {
|
|
1086
|
+
children = await readdir(folder, { withFileTypes: true });
|
|
1087
|
+
} catch {
|
|
1088
|
+
return found;
|
|
1089
|
+
}
|
|
1090
|
+
for (const entry of children) {
|
|
1091
|
+
const path = join3(folder, entry.name);
|
|
1092
|
+
if (entry.isDirectory() && depth < KNOWN_DEPTH && !SKIPPED.has(entry.name))
|
|
1093
|
+
found.push(...await envFilesBelow(path, depth + 1));
|
|
1094
|
+
else if (ENV_FILE.test(entry.name) && await isFileEntry(entry, path))
|
|
1095
|
+
found.push(path);
|
|
1096
|
+
}
|
|
1097
|
+
return found;
|
|
1098
|
+
}
|
|
1099
|
+
async function loadKnown(folder = process.cwd(), extra = []) {
|
|
1100
|
+
const pairs = extra.flatMap(secretPairs);
|
|
1101
|
+
for (const path of await envFilesBelow(folder)) {
|
|
1102
|
+
try {
|
|
1103
|
+
pairs.push(...secretPairs(parseDotenv(await Bun.file(path).text())));
|
|
1104
|
+
} catch {}
|
|
1105
|
+
}
|
|
1106
|
+
return knownSecrets(pairs);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// ../env/src/redactor.ts
|
|
1110
|
+
var MAX_PENDING = 1 << 18;
|
|
1111
|
+
var OVERLAP = 1 << 13;
|
|
1112
|
+
|
|
1113
|
+
class Redactor {
|
|
1114
|
+
known;
|
|
1115
|
+
pending = "";
|
|
1116
|
+
inKey = false;
|
|
1117
|
+
hidden = false;
|
|
1118
|
+
constructor(known) {
|
|
1119
|
+
this.known = known;
|
|
1120
|
+
}
|
|
1121
|
+
push(chunk) {
|
|
1122
|
+
const end = chunk.lastIndexOf(`
|
|
1123
|
+
`);
|
|
1124
|
+
this.pending += chunk;
|
|
1125
|
+
if (end === -1)
|
|
1126
|
+
return this.pending.length > MAX_PENDING ? this.cut() : "";
|
|
1127
|
+
const split = this.pending.length - chunk.length + end + 1;
|
|
1128
|
+
const complete = this.pending.slice(0, split);
|
|
1129
|
+
this.pending = this.pending.slice(split);
|
|
1130
|
+
return complete.split(`
|
|
1131
|
+
`).slice(0, -1).map((line) => this.line(line)).filter((line) => line !== undefined).map((line) => `${line}
|
|
1132
|
+
`).join("");
|
|
1133
|
+
}
|
|
1134
|
+
cut() {
|
|
1135
|
+
const limit = this.pending.length - OVERLAP;
|
|
1136
|
+
const space = this.pending.lastIndexOf(" ", limit);
|
|
1137
|
+
const at = space > 0 ? space + 1 : limit;
|
|
1138
|
+
const head = this.pending.slice(0, at);
|
|
1139
|
+
this.pending = this.pending.slice(at);
|
|
1140
|
+
return this.line(head) ?? "";
|
|
1141
|
+
}
|
|
1142
|
+
end() {
|
|
1143
|
+
const rest = this.pending;
|
|
1144
|
+
this.pending = "";
|
|
1145
|
+
return rest === "" ? "" : this.line(rest) ?? "";
|
|
1146
|
+
}
|
|
1147
|
+
line(line) {
|
|
1148
|
+
if (this.inKey) {
|
|
1149
|
+
const end = PEM_END.exec(line);
|
|
1150
|
+
if (end === null) {
|
|
1151
|
+
if (this.hidden)
|
|
1152
|
+
return;
|
|
1153
|
+
this.hidden = true;
|
|
1154
|
+
return KEY_MASK;
|
|
1155
|
+
}
|
|
1156
|
+
this.inKey = false;
|
|
1157
|
+
return (end.index > 0 && !this.hidden ? KEY_MASK : "") + this.line(line.slice(end.index));
|
|
1158
|
+
}
|
|
1159
|
+
const body = openKeyBody(line);
|
|
1160
|
+
if (body !== undefined) {
|
|
1161
|
+
this.inKey = true;
|
|
1162
|
+
this.hidden = body !== "";
|
|
1163
|
+
}
|
|
1164
|
+
return maskLine(line, this.known);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/stack.ts
|
|
756
1169
|
var LABEL_FORMAT = ["project", "project.config_files", "project.environment_file", "project.working_dir"].map((label) => `{{.Label "com.docker.compose.${label}"}}`).join("\t");
|
|
757
1170
|
var COMPOSE_FILE_VAR = /^\s*(?:export\s+)?COMPOSE_FILE\s*=\s*\S/m;
|
|
758
1171
|
var COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
759
|
-
var
|
|
1172
|
+
var SKIPPED2 = new Set(["node_modules", "vendor", "dist", "build", "target", "venv", "__pycache__"]);
|
|
760
1173
|
var ACTIONS = ["shell", "logs", "restart", "up", "stop", "build"];
|
|
761
1174
|
function from(dir, command) {
|
|
762
1175
|
return dir === "." ? command : ["env", "-C", dir, ...command];
|
|
763
1176
|
}
|
|
764
1177
|
function children(dir = ".") {
|
|
765
|
-
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !
|
|
1178
|
+
return readdirSync(dir, { withFileTypes: true }).filter((child) => child.isDirectory() && !child.name.startsWith(".") && !SKIPPED2.has(child.name)).map((child) => child.name).sort();
|
|
766
1179
|
}
|
|
767
1180
|
function up(dir) {
|
|
768
1181
|
const dirs = [];
|
|
769
|
-
for (let current = dir;; current =
|
|
1182
|
+
for (let current = dir;; current = join4(current, "..")) {
|
|
770
1183
|
dirs.push(current);
|
|
771
1184
|
const absolute = resolve(current);
|
|
772
|
-
if (existsSync(
|
|
1185
|
+
if (existsSync(join4(current, ".git")) || dirname4(absolute) === absolute)
|
|
773
1186
|
return dirs;
|
|
774
1187
|
}
|
|
775
1188
|
}
|
|
@@ -777,10 +1190,10 @@ function stackAt(dir) {
|
|
|
777
1190
|
return { dir, run: from(dir, ["docker", "compose"]), list: { argv: ["docker", "compose"], cwd: resolve(dir), env: process.env } };
|
|
778
1191
|
}
|
|
779
1192
|
function holdsStack(dir) {
|
|
780
|
-
if (COMPOSE_FILES.some((name) => existsSync(
|
|
1193
|
+
if (COMPOSE_FILES.some((name) => existsSync(join4(dir, name))))
|
|
781
1194
|
return true;
|
|
782
1195
|
try {
|
|
783
|
-
return COMPOSE_FILE_VAR.test(
|
|
1196
|
+
return COMPOSE_FILE_VAR.test(readFileSync2(join4(dir, ".env"), "utf-8"));
|
|
784
1197
|
} catch {
|
|
785
1198
|
return false;
|
|
786
1199
|
}
|
|
@@ -827,9 +1240,12 @@ async function recordedNearby() {
|
|
|
827
1240
|
const dirs = new Set([...up("."), ...children()].map((dir) => resolve(dir)));
|
|
828
1241
|
return (await labels()).filter((stack) => dirs.has(stack.workdir));
|
|
829
1242
|
}
|
|
1243
|
+
async function attempt(stack, args) {
|
|
1244
|
+
const result = await captureOutput([...stack.list.argv, ...args], { cwd: stack.list.cwd, env: stack.list.env });
|
|
1245
|
+
return result.code === 0 ? { text: result.stdout, error: undefined } : { text: "", error: composeError(result) };
|
|
1246
|
+
}
|
|
830
1247
|
async function output(stack, args) {
|
|
831
|
-
|
|
832
|
-
return await proc.exited === 0 ? new Response(proc.stdout).text() : "";
|
|
1248
|
+
return (await attempt(stack, args)).text;
|
|
833
1249
|
}
|
|
834
1250
|
function containers(text) {
|
|
835
1251
|
const trimmed = text.trim();
|
|
@@ -870,13 +1286,14 @@ function key(service) {
|
|
|
870
1286
|
async function shellOf(service) {
|
|
871
1287
|
return await output(service.stack, ["exec", "-T", service.name, "sh", "-c", "command -v bash"]) ? "bash" : "sh";
|
|
872
1288
|
}
|
|
873
|
-
|
|
1289
|
+
var LOGS_TAIL = 200;
|
|
1290
|
+
async function commandFor(service, action, logs = {}) {
|
|
874
1291
|
const run = service.stack.run;
|
|
875
1292
|
switch (action) {
|
|
876
1293
|
case "shell":
|
|
877
1294
|
return [...run, "exec", service.name, await shellOf(service)];
|
|
878
1295
|
case "logs":
|
|
879
|
-
return [...run, "logs", "-f",
|
|
1296
|
+
return [...run, "logs", ...logs.follow === false ? [] : ["-f"], `--tail=${logs.tail ?? LOGS_TAIL}`, service.name];
|
|
880
1297
|
case "restart":
|
|
881
1298
|
return [...run, "restart", service.name];
|
|
882
1299
|
case "up":
|
|
@@ -902,8 +1319,12 @@ async function serviceNamed(dir, name, project) {
|
|
|
902
1319
|
const stack = recorded === undefined ? stackAt(dir) : labelledStack(dir, recorded);
|
|
903
1320
|
return (await services(stack)).find((candidate) => candidate.name === name);
|
|
904
1321
|
}
|
|
1322
|
+
function maskLog(text, known) {
|
|
1323
|
+
const redactor = new Redactor(known);
|
|
1324
|
+
return redactor.push(text) + redactor.end();
|
|
1325
|
+
}
|
|
905
1326
|
async function logsOf(stack, name) {
|
|
906
|
-
return output(stack, ["logs", "--tail=30", "--no-log-prefix", name]);
|
|
1327
|
+
return maskLog(await output(stack, ["logs", "--tail=30", "--no-log-prefix", name]), await loadKnown());
|
|
907
1328
|
}
|
|
908
1329
|
|
|
909
1330
|
// src/dc.ts
|
|
@@ -965,4 +1386,4 @@ async function main(args) {
|
|
|
965
1386
|
}
|
|
966
1387
|
return dc(args);
|
|
967
1388
|
}
|
|
968
|
-
process9.exitCode = await runMain({ name: "dc",
|
|
1389
|
+
process9.exitCode = await runMain({ name: "dc", run: main });
|