@kaynooo/guard-tools 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +951 -146
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { dirname } from "path";
|
|
8
|
-
import
|
|
8
|
+
import process7 from "process";
|
|
9
9
|
|
|
10
10
|
// ../kit/src/args.ts
|
|
11
11
|
import process3 from "process";
|
|
@@ -504,8 +504,103 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
504
504
|
fail(`no file ${path}`);
|
|
505
505
|
return file.text();
|
|
506
506
|
}
|
|
507
|
-
// ../kit/src/
|
|
507
|
+
// ../kit/src/process.ts
|
|
508
|
+
import { constants } from "os";
|
|
508
509
|
import process4 from "process";
|
|
510
|
+
var KILL_GRACE_MS = 2000;
|
|
511
|
+
function exitCodeOf(code, signal) {
|
|
512
|
+
if (code !== null)
|
|
513
|
+
return code;
|
|
514
|
+
if (typeof signal === "number")
|
|
515
|
+
return 128 + signal;
|
|
516
|
+
return signal === null ? 129 : 128 + constants.signals[signal];
|
|
517
|
+
}
|
|
518
|
+
async function runCommand(argv, { cwd, env, stdin = "ignore", timeoutMs, signal, maxBytes = Infinity, onChunk, onStart } = {}) {
|
|
519
|
+
const chunks = [];
|
|
520
|
+
const grouped = timeoutMs !== undefined || signal !== undefined || onStart !== undefined || onChunk !== undefined;
|
|
521
|
+
const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe", ...grouped ? { detached: true } : {} });
|
|
522
|
+
const state = { kept: 0, truncated: false, timedOut: false, failure: undefined, done: false };
|
|
523
|
+
const timers = [];
|
|
524
|
+
const killGroup = (killing) => {
|
|
525
|
+
if (state.done)
|
|
526
|
+
return;
|
|
527
|
+
try {
|
|
528
|
+
if (grouped)
|
|
529
|
+
process4.kill(-proc.pid, killing);
|
|
530
|
+
else
|
|
531
|
+
proc.kill(killing);
|
|
532
|
+
} catch {}
|
|
533
|
+
};
|
|
534
|
+
const stop = () => {
|
|
535
|
+
killGroup("SIGTERM");
|
|
536
|
+
timers.push(setTimeout(killGroup, KILL_GRACE_MS, "SIGKILL"));
|
|
537
|
+
};
|
|
538
|
+
const onAbort = () => stop();
|
|
539
|
+
if (timeoutMs !== undefined) {
|
|
540
|
+
timers.push(setTimeout(() => {
|
|
541
|
+
state.timedOut = true;
|
|
542
|
+
stop();
|
|
543
|
+
}, timeoutMs));
|
|
544
|
+
}
|
|
545
|
+
if (signal?.aborted === true)
|
|
546
|
+
onAbort();
|
|
547
|
+
else
|
|
548
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
549
|
+
onStart?.({ kill: (killing) => killGroup(killing) });
|
|
550
|
+
const drain = async (stream, name) => {
|
|
551
|
+
for await (const data of stream) {
|
|
552
|
+
if (state.failure !== undefined)
|
|
553
|
+
continue;
|
|
554
|
+
try {
|
|
555
|
+
onChunk?.({ stream: name, data });
|
|
556
|
+
} catch (error) {
|
|
557
|
+
state.failure = error;
|
|
558
|
+
stop();
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (state.kept + data.length > maxBytes) {
|
|
562
|
+
state.truncated = true;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
state.kept += data.length;
|
|
566
|
+
chunks.push({ stream: name, data });
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
try {
|
|
570
|
+
await Promise.all([drain(proc.stdout, "stdout"), drain(proc.stderr, "stderr")]);
|
|
571
|
+
await proc.exited;
|
|
572
|
+
} finally {
|
|
573
|
+
state.done = true;
|
|
574
|
+
for (const timer of timers)
|
|
575
|
+
clearTimeout(timer);
|
|
576
|
+
signal?.removeEventListener("abort", onAbort);
|
|
577
|
+
}
|
|
578
|
+
if (state.failure !== undefined)
|
|
579
|
+
throw state.failure;
|
|
580
|
+
const joined = () => Buffer.concat(chunks.map(({ data }) => data));
|
|
581
|
+
let output;
|
|
582
|
+
let text;
|
|
583
|
+
return {
|
|
584
|
+
code: exitCodeOf(proc.exitCode, proc.signalCode),
|
|
585
|
+
chunks,
|
|
586
|
+
timedOut: state.timedOut,
|
|
587
|
+
truncated: state.truncated,
|
|
588
|
+
get output() {
|
|
589
|
+
return output ??= joined();
|
|
590
|
+
},
|
|
591
|
+
get text() {
|
|
592
|
+
return text ??= (output ??= joined()).toString();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ../kit/src/progress.ts
|
|
598
|
+
var ESC = String.fromCharCode(27);
|
|
599
|
+
var ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, "y");
|
|
600
|
+
var RESET_STYLE = `${ESC}[0m`;
|
|
601
|
+
var CLOSE_LINK = `${ESC}]8;;${ESC}\\`;
|
|
602
|
+
// ../kit/src/cli.ts
|
|
603
|
+
import process5 from "process";
|
|
509
604
|
var LEVEL_OPTIONS = {
|
|
510
605
|
...OUTPUT_OPTIONS,
|
|
511
606
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
@@ -581,8 +676,8 @@ async function help({ path, header, summary: lazySummary, commands, defaultComma
|
|
|
581
676
|
`);
|
|
582
677
|
}
|
|
583
678
|
function printError(prefix, message, hint, cause) {
|
|
584
|
-
const colors = paint(
|
|
585
|
-
const styled = takesControlCodes(
|
|
679
|
+
const colors = paint(process5.stderr);
|
|
680
|
+
const styled = takesControlCodes(process5.stderr);
|
|
586
681
|
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
587
682
|
const under = (label, text, tint) => text === undefined ? "" : `
|
|
588
683
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
@@ -615,11 +710,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
615
710
|
}
|
|
616
711
|
const [name, ...args] = argv;
|
|
617
712
|
if (name === undefined && level.path.length > 1) {
|
|
618
|
-
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]),
|
|
713
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stderr));
|
|
619
714
|
return 2;
|
|
620
715
|
}
|
|
621
716
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
622
|
-
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]),
|
|
717
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stdout));
|
|
623
718
|
return 0;
|
|
624
719
|
}
|
|
625
720
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -692,49 +787,80 @@ async function printCandidates(found) {
|
|
|
692
787
|
console.log(line);
|
|
693
788
|
return 0;
|
|
694
789
|
}
|
|
695
|
-
async function runCli(cli, argv =
|
|
790
|
+
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
696
791
|
if (argv[0] === COMPLETE)
|
|
697
792
|
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
698
793
|
return dispatch({ path: [cli.name], header: (colors, about) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${about}`, summary: cli.about, ...scopeOf(cli.commands, cli.extraCommands, cli.aliases), defaultCommand: cli.defaultCommand, examples: cli.examples, replaces: cli.replaces }, argv, { version: cli.version, redact: cli.redact });
|
|
699
794
|
}
|
|
700
|
-
// ../kit/src/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
chunks.push({ stream: name, data });
|
|
705
|
-
}
|
|
706
|
-
function exitCodeOf(code, signal) {
|
|
707
|
-
if (code !== null)
|
|
708
|
-
return code;
|
|
709
|
-
if (typeof signal === "number")
|
|
710
|
-
return 128 + signal;
|
|
711
|
-
return signal === null ? 129 : 128 + constants.signals[signal];
|
|
712
|
-
}
|
|
713
|
-
async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
|
|
714
|
-
const chunks = [];
|
|
715
|
-
const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe" });
|
|
716
|
-
await Promise.all([collect(proc.stdout, "stdout", chunks), collect(proc.stderr, "stderr", chunks)]);
|
|
717
|
-
await proc.exited;
|
|
718
|
-
const output = Buffer.concat(chunks.map(({ data }) => data));
|
|
719
|
-
return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
|
|
720
|
-
}
|
|
795
|
+
// ../kit/src/logs.ts
|
|
796
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
797
|
+
// ../kit/src/secrets.ts
|
|
798
|
+
var SECRET_WORDS = new Set(["key", "pass", "pwd", "auth", "salt", "cookie", "session", "jwt", "ssn", "iban", "card", "cvv", "otp"]);
|
|
721
799
|
// package.json
|
|
722
|
-
var version = "0.
|
|
800
|
+
var version = "0.5.0";
|
|
723
801
|
|
|
724
802
|
// ../kit/src/shell.ts
|
|
725
803
|
var SEPARATORS = new Set([`
|
|
726
804
|
`, ";", "|"]);
|
|
727
805
|
var PLAIN_BREAKERS = new Set(["<", ">", "(", ")", "&", "|"]);
|
|
806
|
+
var GLOB_CHARS = new Set(["*", "?", "["]);
|
|
807
|
+
var COMMENT_PREFIXES = new Set([";", "&", "|", "(", ")"]);
|
|
728
808
|
var WHITESPACE = /\s/;
|
|
729
809
|
var ASSIGNMENT = /^\w+=/;
|
|
730
810
|
var TRAILING_SLASHES = /\/+$/;
|
|
811
|
+
var TRAILING_CR = /\r$/;
|
|
731
812
|
var LEADING_TILDE = /^~(?=\/|$)/;
|
|
732
813
|
var LEADING_TABS = /^\t+/;
|
|
733
814
|
var OPENERS = /^[({]+/;
|
|
734
815
|
var CLOSERS = /[)}]+$/;
|
|
816
|
+
var CASE_LABEL = /^\(?[^\s()]*\)$/;
|
|
817
|
+
var REDIRECT_ONLY = /^\d*(?:<|>|>>|<&|>&|&>|>\||<>)$/;
|
|
818
|
+
var REDIRECT_START = /^\d*[<>]|^&>/;
|
|
819
|
+
var SAFE_WORD = /^[\w@%+=:,./-]+$/;
|
|
820
|
+
var LEADING_HEX = /^[\da-f]*/i;
|
|
735
821
|
var DOUBLE_QUOTED_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
736
822
|
var HEREDOC = /^<<(-?)[ \t]*(?:'([^']*)'|"([^"]*)"|([^\s;&|<>()-][^\s;&|<>()]*))/;
|
|
737
|
-
var
|
|
823
|
+
var ANSI_QUOTE = "$'";
|
|
824
|
+
var ANSI_ESCAPES = { n: `
|
|
825
|
+
`, t: "\t", r: "\r", a: "\x07", b: "\b", f: "\f", v: "\v", e: "\x1B", E: "\x1B", "\\": "\\", "'": "'", '"': '"', "?": "?" };
|
|
826
|
+
var ANSI_DIGITS = { x: 2, u: 4, U: 8 };
|
|
827
|
+
var OCTAL = /^[0-7]{1,3}/;
|
|
828
|
+
var KEYWORDS = new Set(["then", "do", "else", "if", "elif", "while", "until", "!", "{", "}"]);
|
|
829
|
+
var MAX_DEPTH = 8;
|
|
830
|
+
var MAX_BRACES = 256;
|
|
831
|
+
var SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh"]);
|
|
832
|
+
var COMMAND_STRING_PROGRAMS = new Set([...SHELLS, "su", "runuser"]);
|
|
833
|
+
var SHELL_VALUE_OPTIONS = new Set(["-o", "-O", "--rcfile", "--init-file"]);
|
|
834
|
+
var PRINTERS = new Set(["echo", "printf"]);
|
|
835
|
+
var FIND_EXEC = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
836
|
+
function wrapper(short = "", long = [], operands = 0) {
|
|
837
|
+
return { short, long, operands };
|
|
838
|
+
}
|
|
839
|
+
var WRAPPERS = new Map([
|
|
840
|
+
["command", wrapper()],
|
|
841
|
+
["builtin", wrapper()],
|
|
842
|
+
["nohup", wrapper()],
|
|
843
|
+
["setsid", wrapper()],
|
|
844
|
+
["busybox", wrapper()],
|
|
845
|
+
["exec", wrapper("a")],
|
|
846
|
+
["time", wrapper("fo", ["--format", "--output"])],
|
|
847
|
+
["env", wrapper("uCS", ["--unset", "--chdir", "--split-string", "--argv0"])],
|
|
848
|
+
["sudo", wrapper("ughpCTrtUDR", ["--user", "--group", "--host", "--prompt", "--close-from", "--command-timeout", "--role", "--type", "--other-user", "--chdir", "--chroot"])],
|
|
849
|
+
["doas", wrapper("uC")],
|
|
850
|
+
["timeout", wrapper("sk", ["--signal", "--kill-after"], 1)],
|
|
851
|
+
["nice", wrapper("n", ["--adjustment"])],
|
|
852
|
+
["ionice", wrapper("cnpPu", ["--class", "--classdata", "--pid", "--pgid", "--uid"])],
|
|
853
|
+
["stdbuf", wrapper("ioe", ["--input", "--output", "--error"])],
|
|
854
|
+
["chroot", wrapper("", ["--userspec", "--groups"], 1)],
|
|
855
|
+
["xargs", wrapper("IndsEaPL", ["--max-args", "--max-procs", "--max-chars", "--delimiter", "--arg-file", "--eof", "--max-lines", "--process-slot-var"])]
|
|
856
|
+
]);
|
|
857
|
+
function closerOf(quote) {
|
|
858
|
+
return quote === ANSI_QUOTE ? "'" : quote;
|
|
859
|
+
}
|
|
860
|
+
function startsComment(command, i) {
|
|
861
|
+
const before = command[i - 1] ?? "";
|
|
862
|
+
return i === 0 || WHITESPACE.test(before) || COMMENT_PREFIXES.has(before);
|
|
863
|
+
}
|
|
738
864
|
function separatorWidth(command, i) {
|
|
739
865
|
const pair = command.slice(i, i + 2);
|
|
740
866
|
if (pair === "&&" || pair === "||")
|
|
@@ -751,7 +877,7 @@ function heredocEnd(command, newline, words) {
|
|
|
751
877
|
const next = command.indexOf(`
|
|
752
878
|
`, lineStart);
|
|
753
879
|
const lineEnd = next === -1 ? command.length : next;
|
|
754
|
-
const line = command.slice(lineStart, lineEnd);
|
|
880
|
+
const line = command.slice(lineStart, lineEnd).replace(TRAILING_CR, "");
|
|
755
881
|
lineStart = lineEnd + 1;
|
|
756
882
|
if ((stripsTabs ? line.replace(LEADING_TABS, "") : line) === word)
|
|
757
883
|
break;
|
|
@@ -773,6 +899,7 @@ function withoutRanges(command, start, end, ranges) {
|
|
|
773
899
|
function splitCommand(command) {
|
|
774
900
|
const bounds = [];
|
|
775
901
|
const bodies = [];
|
|
902
|
+
const comments = [];
|
|
776
903
|
let heredocs = [];
|
|
777
904
|
let start = 0;
|
|
778
905
|
let quote = null;
|
|
@@ -783,14 +910,27 @@ function splitCommand(command) {
|
|
|
783
910
|
continue;
|
|
784
911
|
}
|
|
785
912
|
if (quote !== null) {
|
|
786
|
-
if (char === quote)
|
|
913
|
+
if (char === closerOf(quote))
|
|
787
914
|
quote = null;
|
|
788
915
|
continue;
|
|
789
916
|
}
|
|
917
|
+
if (char === "$" && command[i + 1] === "'") {
|
|
918
|
+
quote = ANSI_QUOTE;
|
|
919
|
+
i++;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
790
922
|
if (char === "'" || char === '"') {
|
|
791
923
|
quote = char;
|
|
792
924
|
continue;
|
|
793
925
|
}
|
|
926
|
+
if (char === "#" && startsComment(command, i)) {
|
|
927
|
+
const newline = command.indexOf(`
|
|
928
|
+
`, i);
|
|
929
|
+
const end = newline === -1 ? command.length : newline;
|
|
930
|
+
comments.push([i, end]);
|
|
931
|
+
i = end - 1;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
794
934
|
const heredoc = char === "<" && command[i + 1] === "<" && command[i + 2] !== "<" ? HEREDOC.exec(command.slice(i)) : null;
|
|
795
935
|
if (heredoc) {
|
|
796
936
|
heredocs.push({ word: heredoc[2] ?? heredoc[3] ?? heredoc[4] ?? "", stripsTabs: heredoc[1] === "-", isQuoted: heredoc[4] === undefined });
|
|
@@ -813,32 +953,78 @@ function splitCommand(command) {
|
|
|
813
953
|
start = i + 1;
|
|
814
954
|
}
|
|
815
955
|
bounds.push([start, command.length, ""]);
|
|
816
|
-
const ranges = bodies.map(([from, to]) => [from, to]);
|
|
817
|
-
return bounds.map(([from, to, separator]) =>
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
956
|
+
const ranges = [...bodies, ...comments].map(([from, to]) => [from, to]).sort(([a], [b]) => a - b);
|
|
957
|
+
return bounds.map(([from, to, separator]) => {
|
|
958
|
+
const inside = bodies.filter(([body]) => body >= from && body < to);
|
|
959
|
+
return {
|
|
960
|
+
text: command.slice(from, to),
|
|
961
|
+
start: from,
|
|
962
|
+
end: to,
|
|
963
|
+
command: withoutRanges(command, from, to, ranges),
|
|
964
|
+
separator,
|
|
965
|
+
expanded: inside.filter(([, , expands]) => expands).map(([body, bodyEnd]) => command.slice(body, bodyEnd)).join(""),
|
|
966
|
+
heredocs: inside.map(([body, bodyEnd]) => command.slice(body, bodyEnd))
|
|
967
|
+
};
|
|
968
|
+
});
|
|
825
969
|
}
|
|
826
970
|
function isPlain(text) {
|
|
827
971
|
let quote = null;
|
|
828
972
|
for (let i = 0;i < text.length; i++) {
|
|
829
973
|
const char = text[i] ?? "";
|
|
830
974
|
const isSubstitution = char === "`" || char === "$" && text[i + 1] === "(";
|
|
975
|
+
if (quote === null && char === "$" && text[i + 1] === "'") {
|
|
976
|
+
quote = ANSI_QUOTE;
|
|
977
|
+
i++;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
831
980
|
if (char === "\\" && quote !== "'")
|
|
832
981
|
i++;
|
|
833
|
-
else if (quote !== null && char === quote)
|
|
982
|
+
else if (quote !== null && char === closerOf(quote))
|
|
834
983
|
quote = null;
|
|
835
984
|
else if (quote === null && (char === '"' || char === "'"))
|
|
836
985
|
quote = char;
|
|
837
|
-
else if (quote !== "'" && (isSubstitution || quote === null && PLAIN_BREAKERS.has(char)))
|
|
986
|
+
else if (quote !== "'" && quote !== ANSI_QUOTE && (isSubstitution || quote === null && PLAIN_BREAKERS.has(char)))
|
|
838
987
|
return false;
|
|
839
988
|
}
|
|
840
989
|
return quote === null;
|
|
841
990
|
}
|
|
991
|
+
function unmatchedClosers(text) {
|
|
992
|
+
let depth = 0;
|
|
993
|
+
let unmatched = 0;
|
|
994
|
+
let quote = null;
|
|
995
|
+
for (let i = 0;i < text.length; i++) {
|
|
996
|
+
const char = text[i] ?? "";
|
|
997
|
+
if (quote === null && char === "$" && text[i + 1] === "'") {
|
|
998
|
+
quote = ANSI_QUOTE;
|
|
999
|
+
i++;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (char === "\\" && quote !== "'")
|
|
1003
|
+
i++;
|
|
1004
|
+
else if (quote !== null && char === closerOf(quote))
|
|
1005
|
+
quote = null;
|
|
1006
|
+
else if (quote === null && (char === '"' || char === "'"))
|
|
1007
|
+
quote = char;
|
|
1008
|
+
else if (quote === null && char === "(")
|
|
1009
|
+
depth++;
|
|
1010
|
+
else if (quote === null && char === ")" && depth > 0)
|
|
1011
|
+
depth--;
|
|
1012
|
+
else if (quote === null && char === ")")
|
|
1013
|
+
unmatched++;
|
|
1014
|
+
}
|
|
1015
|
+
return unmatched;
|
|
1016
|
+
}
|
|
1017
|
+
function withoutSubshell(text) {
|
|
1018
|
+
let result = text.trim();
|
|
1019
|
+
let closers = unmatchedClosers(result);
|
|
1020
|
+
while (result.startsWith("("))
|
|
1021
|
+
result = result.slice(1).trimStart();
|
|
1022
|
+
while (closers > 0 && result.endsWith(")")) {
|
|
1023
|
+
result = result.slice(0, -1).trimEnd();
|
|
1024
|
+
closers--;
|
|
1025
|
+
}
|
|
1026
|
+
return result;
|
|
1027
|
+
}
|
|
842
1028
|
function closingParen(text, open) {
|
|
843
1029
|
let depth = 0;
|
|
844
1030
|
let quote = null;
|
|
@@ -847,8 +1033,11 @@ function closingParen(text, open) {
|
|
|
847
1033
|
if (char === "\\" && quote !== "'") {
|
|
848
1034
|
i++;
|
|
849
1035
|
} else if (quote !== null) {
|
|
850
|
-
if (char === quote)
|
|
1036
|
+
if (char === closerOf(quote))
|
|
851
1037
|
quote = null;
|
|
1038
|
+
} else if (char === "$" && text[i + 1] === "'") {
|
|
1039
|
+
quote = ANSI_QUOTE;
|
|
1040
|
+
i++;
|
|
852
1041
|
} else if (char === "'" || char === '"') {
|
|
853
1042
|
quote = char;
|
|
854
1043
|
} else if (char === "(") {
|
|
@@ -873,12 +1062,18 @@ function bodies(text, isQuoting) {
|
|
|
873
1062
|
let quote = null;
|
|
874
1063
|
for (let i = 0;i < text.length; i++) {
|
|
875
1064
|
const char = text[i] ?? "";
|
|
1065
|
+
const next = text[i + 1];
|
|
876
1066
|
if (quote === "'") {
|
|
877
1067
|
if (char === "'")
|
|
878
1068
|
quote = null;
|
|
1069
|
+
} else if (quote === ANSI_QUOTE) {
|
|
1070
|
+
if (char === "\\")
|
|
1071
|
+
i++;
|
|
1072
|
+
else if (char === "'")
|
|
1073
|
+
quote = null;
|
|
879
1074
|
} else if (char === "\\") {
|
|
880
1075
|
i++;
|
|
881
|
-
} else if (char === "$" &&
|
|
1076
|
+
} else if (char === "$" && next === "(" || isQuoting && quote === null && (char === "<" || char === ">") && next === "(") {
|
|
882
1077
|
const end = closingParen(text, i + 1);
|
|
883
1078
|
found.push(text.slice(i + 2, end));
|
|
884
1079
|
i = end;
|
|
@@ -888,6 +1083,9 @@ function bodies(text, isQuoting) {
|
|
|
888
1083
|
i = end;
|
|
889
1084
|
} else if (isQuoting && char === '"') {
|
|
890
1085
|
quote = quote === null ? char : null;
|
|
1086
|
+
} else if (isQuoting && char === "$" && next === "'" && quote === null) {
|
|
1087
|
+
quote = ANSI_QUOTE;
|
|
1088
|
+
i++;
|
|
891
1089
|
} else if (isQuoting && char === "'" && quote === null) {
|
|
892
1090
|
quote = char;
|
|
893
1091
|
}
|
|
@@ -897,11 +1095,26 @@ function bodies(text, isQuoting) {
|
|
|
897
1095
|
function substitutions(command) {
|
|
898
1096
|
return splitCommand(command).flatMap((piece) => [...bodies(piece.command, true), ...bodies(piece.expanded, false)]);
|
|
899
1097
|
}
|
|
1098
|
+
function ansiEscape(text, at) {
|
|
1099
|
+
const letter = text[at + 1] ?? "";
|
|
1100
|
+
const simple = ANSI_ESCAPES[letter];
|
|
1101
|
+
if (simple !== undefined)
|
|
1102
|
+
return [simple, at + 1];
|
|
1103
|
+
const hex = LEADING_HEX.exec(text.slice(at + 2, at + 2 + (ANSI_DIGITS[letter] ?? 0)))?.[0] ?? "";
|
|
1104
|
+
if (hex !== "")
|
|
1105
|
+
return [String.fromCodePoint(Number.parseInt(hex, 16)), at + 1 + hex.length];
|
|
1106
|
+
const octal = OCTAL.exec(text.slice(at + 1))?.[0];
|
|
1107
|
+
if (octal !== undefined)
|
|
1108
|
+
return [String.fromCodePoint(Number.parseInt(octal, 8)), at + octal.length];
|
|
1109
|
+
return [`\\${letter}`, at + 1];
|
|
1110
|
+
}
|
|
900
1111
|
function words(text) {
|
|
901
1112
|
const found = [];
|
|
902
1113
|
let current = "";
|
|
903
1114
|
let isStarted = false;
|
|
904
1115
|
let isProse = false;
|
|
1116
|
+
let hasGlob = false;
|
|
1117
|
+
let hasBrace = false;
|
|
905
1118
|
let quote = null;
|
|
906
1119
|
for (let i = 0;i < text.length; i++) {
|
|
907
1120
|
const char = text[i] ?? "";
|
|
@@ -911,40 +1124,79 @@ function words(text) {
|
|
|
911
1124
|
i++;
|
|
912
1125
|
if (quote === null && (isContinuation || WHITESPACE.test(char))) {
|
|
913
1126
|
if (isStarted)
|
|
914
|
-
found.push({ text: current, isProse });
|
|
1127
|
+
found.push({ text: current, isProse, hasGlob, hasBrace });
|
|
915
1128
|
current = "";
|
|
916
1129
|
isStarted = false;
|
|
917
1130
|
isProse = false;
|
|
1131
|
+
hasGlob = false;
|
|
1132
|
+
hasBrace = false;
|
|
918
1133
|
continue;
|
|
919
1134
|
}
|
|
920
1135
|
if (isContinuation)
|
|
921
1136
|
continue;
|
|
922
1137
|
isStarted = true;
|
|
923
|
-
if (char === "\\" && quote ===
|
|
1138
|
+
if (char === "\\" && quote === ANSI_QUOTE) {
|
|
1139
|
+
const [decoded, last] = ansiEscape(text, i);
|
|
1140
|
+
current += decoded;
|
|
1141
|
+
i = last;
|
|
1142
|
+
} else if (char === "\\" && quote === '"' && !DOUBLE_QUOTED_ESCAPES.has(text[i + 1] ?? "")) {
|
|
924
1143
|
current += char;
|
|
925
|
-
else if (char === "\\" && quote !== "'")
|
|
1144
|
+
} else if (char === "\\" && quote !== "'") {
|
|
926
1145
|
current += text[++i] ?? "";
|
|
927
|
-
else if (quote !== null && char === quote)
|
|
1146
|
+
} else if (quote !== null && char === closerOf(quote)) {
|
|
928
1147
|
quote = null;
|
|
929
|
-
else if (quote === null &&
|
|
1148
|
+
} else if (quote === null && char === "$" && text[i + 1] === "'") {
|
|
1149
|
+
quote = ANSI_QUOTE;
|
|
1150
|
+
i++;
|
|
1151
|
+
} else if (quote === null && (char === '"' || char === "'")) {
|
|
930
1152
|
quote = char;
|
|
931
|
-
else
|
|
1153
|
+
} else {
|
|
932
1154
|
current += char;
|
|
1155
|
+
hasGlob ||= quote === null && GLOB_CHARS.has(char);
|
|
1156
|
+
hasBrace ||= quote === null && char === "{";
|
|
1157
|
+
}
|
|
933
1158
|
isProse ||= quote !== null && WHITESPACE.test(char);
|
|
934
1159
|
}
|
|
935
1160
|
if (isStarted)
|
|
936
|
-
found.push({ text: current, isProse });
|
|
1161
|
+
found.push({ text: current, isProse, hasGlob, hasBrace });
|
|
937
1162
|
return found;
|
|
938
1163
|
}
|
|
939
1164
|
function tokenize(text) {
|
|
940
1165
|
return words(text).map((word) => word.text);
|
|
941
1166
|
}
|
|
1167
|
+
function skipOptions(tokens, spec) {
|
|
1168
|
+
const take = (name, attached) => {
|
|
1169
|
+
const value = attached === "" ? tokens.shift() ?? "" : attached;
|
|
1170
|
+
if (name === "S" || name === "--split-string")
|
|
1171
|
+
tokens.unshift(...tokenize(value));
|
|
1172
|
+
};
|
|
1173
|
+
while (tokens[0] !== undefined && tokens[0].startsWith("-") && tokens[0] !== "-") {
|
|
1174
|
+
const option = tokens.shift() ?? "";
|
|
1175
|
+
if (option === "--")
|
|
1176
|
+
break;
|
|
1177
|
+
if (option.startsWith("--")) {
|
|
1178
|
+
const [name = "", ...value] = option.split("=");
|
|
1179
|
+
if (spec.long.includes(name))
|
|
1180
|
+
take(name, value.join("="));
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
for (const [index, letter] of [...option.slice(1)].entries()) {
|
|
1184
|
+
if (!spec.short.includes(letter))
|
|
1185
|
+
continue;
|
|
1186
|
+
take(letter, option.slice(index + 2));
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
for (let operand = 0;operand < spec.operands; operand++)
|
|
1191
|
+
tokens.shift();
|
|
1192
|
+
}
|
|
942
1193
|
function parseCommand(text) {
|
|
943
1194
|
const tokens = tokenize(text);
|
|
944
1195
|
const assignments = [];
|
|
945
1196
|
let isWrapped = false;
|
|
946
1197
|
for (;; ) {
|
|
947
1198
|
const first = tokens[0] ?? "";
|
|
1199
|
+
const spec = WRAPPERS.get(first.split("/").pop() ?? "");
|
|
948
1200
|
if (OPENERS.test(first)) {
|
|
949
1201
|
isWrapped = true;
|
|
950
1202
|
const rest = first.replace(OPENERS, "");
|
|
@@ -954,11 +1206,20 @@ function parseCommand(text) {
|
|
|
954
1206
|
tokens[0] = rest;
|
|
955
1207
|
} else if (ASSIGNMENT.test(first)) {
|
|
956
1208
|
assignments.push(tokens.shift() ?? "");
|
|
957
|
-
} else if (
|
|
1209
|
+
} else if (KEYWORDS.has(first)) {
|
|
958
1210
|
isWrapped = true;
|
|
959
1211
|
tokens.shift();
|
|
960
|
-
|
|
961
|
-
|
|
1212
|
+
} else if (first === "case") {
|
|
1213
|
+
isWrapped = true;
|
|
1214
|
+
const at = tokens.indexOf("in");
|
|
1215
|
+
tokens.splice(0, at === -1 ? tokens.length : at + 1);
|
|
1216
|
+
} else if (tokens.length > 1 && CASE_LABEL.test(first)) {
|
|
1217
|
+
isWrapped = true;
|
|
1218
|
+
tokens.shift();
|
|
1219
|
+
} else if (spec !== undefined) {
|
|
1220
|
+
isWrapped = true;
|
|
1221
|
+
tokens.shift();
|
|
1222
|
+
skipOptions(tokens, spec);
|
|
962
1223
|
} else {
|
|
963
1224
|
break;
|
|
964
1225
|
}
|
|
@@ -972,6 +1233,131 @@ function parseCommand(text) {
|
|
|
972
1233
|
const [program = "", ...args] = tokens;
|
|
973
1234
|
return { program: program.split("/").pop() ?? "", args, assignments, isWrapped };
|
|
974
1235
|
}
|
|
1236
|
+
function quoted(word) {
|
|
1237
|
+
return SAFE_WORD.test(word) || word === "{}" ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
1238
|
+
}
|
|
1239
|
+
function commandStrings(tokens) {
|
|
1240
|
+
const found = [];
|
|
1241
|
+
for (const [i, token] of tokens.entries()) {
|
|
1242
|
+
const name = token.split("/").pop() ?? "";
|
|
1243
|
+
if (!COMMAND_STRING_PROGRAMS.has(name))
|
|
1244
|
+
continue;
|
|
1245
|
+
const isUser = name === "su" || name === "runuser";
|
|
1246
|
+
let isValue = false;
|
|
1247
|
+
for (let j = i + 1;j < tokens.length; j++) {
|
|
1248
|
+
const option = tokens[j] ?? "";
|
|
1249
|
+
if (isValue) {
|
|
1250
|
+
isValue = false;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (SHELL_VALUE_OPTIONS.has(option)) {
|
|
1254
|
+
isValue = true;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (option.startsWith("--command=")) {
|
|
1258
|
+
found.push(option.slice("--command=".length));
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
if (option === "--command" || !option.startsWith("--") && option.startsWith("-") && option.slice(1).includes("c")) {
|
|
1262
|
+
found.push(tokens[j + 1] ?? "");
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
if (!option.startsWith("-") && !isUser)
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return found;
|
|
1270
|
+
}
|
|
1271
|
+
function redirections(args) {
|
|
1272
|
+
const operands = [];
|
|
1273
|
+
const hereStrings = [];
|
|
1274
|
+
for (let i = 0;i < args.length; i++) {
|
|
1275
|
+
const arg = args[i] ?? "";
|
|
1276
|
+
if (arg.startsWith("<<<"))
|
|
1277
|
+
hereStrings.push(arg.length > 3 ? arg.slice(3) : args[++i] ?? "");
|
|
1278
|
+
else if (REDIRECT_ONLY.test(arg))
|
|
1279
|
+
i++;
|
|
1280
|
+
else if (!REDIRECT_START.test(arg))
|
|
1281
|
+
operands.push(arg);
|
|
1282
|
+
}
|
|
1283
|
+
return { operands, hereStrings };
|
|
1284
|
+
}
|
|
1285
|
+
function findExecs(args) {
|
|
1286
|
+
const found = [];
|
|
1287
|
+
for (const [i, arg] of args.entries()) {
|
|
1288
|
+
if (!FIND_EXEC.has(arg))
|
|
1289
|
+
continue;
|
|
1290
|
+
const rest = args.slice(i + 1);
|
|
1291
|
+
const end = rest.findIndex((word) => word === ";" || word === "+");
|
|
1292
|
+
found.push((end === -1 ? rest : rest.slice(0, end)).map(quoted).join(" "));
|
|
1293
|
+
}
|
|
1294
|
+
return found;
|
|
1295
|
+
}
|
|
1296
|
+
function nestedCommands(piece, previous) {
|
|
1297
|
+
const tokens = tokenize(piece.command);
|
|
1298
|
+
const { program, args } = parseCommand(piece.command);
|
|
1299
|
+
const found = commandStrings(tokens);
|
|
1300
|
+
if (program === "eval")
|
|
1301
|
+
found.push(args.join(" "));
|
|
1302
|
+
if (program === "find")
|
|
1303
|
+
found.push(...findExecs(args));
|
|
1304
|
+
if (!SHELLS.has(program))
|
|
1305
|
+
return found;
|
|
1306
|
+
const { operands, hereStrings } = redirections(args);
|
|
1307
|
+
if (operands.length > 0)
|
|
1308
|
+
return found;
|
|
1309
|
+
found.push(...piece.heredocs, ...hereStrings);
|
|
1310
|
+
if (previous?.separator === "|") {
|
|
1311
|
+
const before = parseCommand(previous.command);
|
|
1312
|
+
if (PRINTERS.has(before.program))
|
|
1313
|
+
found.push(...before.args, before.args.join(" "));
|
|
1314
|
+
}
|
|
1315
|
+
return found;
|
|
1316
|
+
}
|
|
1317
|
+
function commandsRun(command, depth = 0) {
|
|
1318
|
+
const found = [command];
|
|
1319
|
+
if (depth >= MAX_DEPTH)
|
|
1320
|
+
return found;
|
|
1321
|
+
const pieces = splitCommand(command);
|
|
1322
|
+
for (const [index, piece] of pieces.entries()) {
|
|
1323
|
+
const inner = withoutSubshell(piece.command);
|
|
1324
|
+
const nested = [...bodies(piece.command, true), ...bodies(piece.expanded, false), ...nestedCommands(piece, pieces[index - 1])];
|
|
1325
|
+
if (inner !== piece.command.trim())
|
|
1326
|
+
nested.push(inner);
|
|
1327
|
+
for (const each of nested)
|
|
1328
|
+
found.push(...commandsRun(each, depth + 1));
|
|
1329
|
+
}
|
|
1330
|
+
return found;
|
|
1331
|
+
}
|
|
1332
|
+
function expandBraces(word) {
|
|
1333
|
+
const open = word.indexOf("{");
|
|
1334
|
+
if (open === -1)
|
|
1335
|
+
return [word];
|
|
1336
|
+
const alternatives = [];
|
|
1337
|
+
let depth = 0;
|
|
1338
|
+
let cut = open;
|
|
1339
|
+
for (let i = open;i < word.length; i++) {
|
|
1340
|
+
const char = word[i];
|
|
1341
|
+
if (char === "{") {
|
|
1342
|
+
depth++;
|
|
1343
|
+
} else if (char === "," && depth === 1) {
|
|
1344
|
+
alternatives.push(word.slice(cut + 1, i));
|
|
1345
|
+
cut = i;
|
|
1346
|
+
} else if (char === "}" && --depth === 0) {
|
|
1347
|
+
if (alternatives.length === 0)
|
|
1348
|
+
break;
|
|
1349
|
+
alternatives.push(word.slice(cut + 1, i));
|
|
1350
|
+
const prefix = word.slice(0, open);
|
|
1351
|
+
const suffix = word.slice(i + 1);
|
|
1352
|
+
return alternatives.flatMap((alternative) => expandBraces(prefix + alternative + suffix)).slice(0, MAX_BRACES + 1);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return expandBraces(word.slice(open + 1)).map((rest) => `${word.slice(0, open + 1)}${rest}`);
|
|
1356
|
+
}
|
|
1357
|
+
function braceExpand(word) {
|
|
1358
|
+
const found = expandBraces(word);
|
|
1359
|
+
return found.length > MAX_BRACES ? [word] : found;
|
|
1360
|
+
}
|
|
975
1361
|
function replacePieces(command, replacements) {
|
|
976
1362
|
let result = command;
|
|
977
1363
|
for (const [piece, text] of [...replacements].sort(([a], [b]) => b.start - a.start)) {
|
|
@@ -1005,10 +1391,14 @@ var LOCK_KIND = /^(?:update|share|no|key)$/;
|
|
|
1005
1391
|
var DIGITS = /\d/;
|
|
1006
1392
|
var COMMAND_ENDS = ";";
|
|
1007
1393
|
var UNCLOSED = "unclosed";
|
|
1394
|
+
var NUL = "\x00";
|
|
1395
|
+
var UNICODE_IDENTIFIER = 'U&"';
|
|
1396
|
+
var UNICODE_PREFIX = /u&$/i;
|
|
1397
|
+
var LINE_END = /[\r\n]/;
|
|
1008
1398
|
var PRIVILEGED_SET = new Set(["role", "session", "transaction"]);
|
|
1009
1399
|
var READ_STARTS = new Set(["select", "with", "values", "table", "show", "explain"]);
|
|
1010
1400
|
var WRITE_WORDS = new Set(["insert", "update", "delete", "merge", "truncate", "drop", "alter", "create", "grant", "revoke", "into", "call", "vacuum", "lock", "refresh", "reindex", "cluster", "do"]);
|
|
1011
|
-
var SIDE_EFFECT_FUNCTIONS = /^(?:nextval|setval|set_config|pg_terminate_backend|pg_cancel_backend|pg_reload_conf|pg_rotate_logfile|pg_switch_wal|pg_create_restore_point|pg_advisory_lock\w*|pg_advisory_xact_lock\w*|pg_try_advisory\w*|pg_notify|lo_\w+|dblink\w*|pg_read_file|pg_read_binary_file|
|
|
1401
|
+
var SIDE_EFFECT_FUNCTIONS = /^(?:nextval|setval|set_config|pg_terminate_backend|pg_cancel_backend|pg_reload_conf|pg_rotate_logfile|pg_switch_wal|pg_create_restore_point|pg_advisory_lock\w*|pg_advisory_xact_lock\w*|pg_try_advisory\w*|pg_notify|lo_\w+|dblink\w*|pg_read_file|pg_read_binary_file|pg_ls_\w+|pg_stat_file|pg_sleep\w*|pg_export_snapshot|pg_backup_start|pg_backup_stop|pg_start_backup|pg_stop_backup|txid_current|pg_current_xact_id|pg_current_logfile|pg_wal_replay_\w+|\w+_to_xml\w*|pg_logical_\w+|pg_replication_\w+|pg_drop_replication_slot|pg_stat_reset\w*|pg_promote|pg_file_\w+)$/;
|
|
1012
1402
|
function tokenize2(sql) {
|
|
1013
1403
|
const tokens = [];
|
|
1014
1404
|
let at = 0;
|
|
@@ -1017,21 +1407,29 @@ function tokenize2(sql) {
|
|
|
1017
1407
|
const next = sql.charAt(at + 1);
|
|
1018
1408
|
if (SPACE.test(char)) {
|
|
1019
1409
|
at++;
|
|
1410
|
+
} else if (char === NUL) {
|
|
1411
|
+
tokens.push({ kind: "punct", text: NUL });
|
|
1412
|
+
at++;
|
|
1020
1413
|
} else if (char === "-" && next === "-") {
|
|
1021
|
-
const end = sql.
|
|
1022
|
-
|
|
1023
|
-
at = end === -1 ? sql.length : end + 1;
|
|
1414
|
+
const end = sql.slice(at).search(LINE_END);
|
|
1415
|
+
at = end === -1 ? sql.length : at + end + 1;
|
|
1024
1416
|
} else if (char === "/" && next === "*" && sql.charAt(at + 2) === "!") {
|
|
1025
1417
|
at += 3;
|
|
1026
1418
|
while (DIGITS.test(sql.charAt(at)))
|
|
1027
1419
|
at++;
|
|
1028
1420
|
} else if (char === "/" && next === "*") {
|
|
1029
1421
|
at = skipComment(sql, at);
|
|
1030
|
-
} else if (char === "'"
|
|
1031
|
-
const backslashes =
|
|
1422
|
+
} else if (char === "'") {
|
|
1423
|
+
const backslashes = ESCAPE_PREFIX.test(sql.charAt(at - 1)) && !WORD_PART.test(sql.charAt(at - 2));
|
|
1032
1424
|
const end = skipQuoted(sql, at, char, backslashes);
|
|
1033
1425
|
at = end ?? sql.length;
|
|
1034
1426
|
tokens.push({ kind: "punct", text: end === undefined ? UNCLOSED : char });
|
|
1427
|
+
} else if (char === '"' || char === "`") {
|
|
1428
|
+
const end = skipQuoted(sql, at, char, false);
|
|
1429
|
+
if (char === '"' && UNICODE_PREFIX.test(sql.slice(Math.max(0, at - 2), at)) && !WORD_PART.test(sql.charAt(at - 3)))
|
|
1430
|
+
tokens.push({ kind: "punct", text: UNICODE_IDENTIFIER });
|
|
1431
|
+
tokens.push(end === undefined ? { kind: "punct", text: UNCLOSED } : { kind: "quoted", text: sql.slice(at + 1, end - 1).replaceAll(char + char, char) });
|
|
1432
|
+
at = end ?? sql.length;
|
|
1035
1433
|
} else if (char === "$" && DOLLAR_TAG.test(sql.slice(at))) {
|
|
1036
1434
|
const tag = DOLLAR_TAG.exec(sql.slice(at))?.[0] ?? "$$";
|
|
1037
1435
|
const end = sql.indexOf(tag, at + tag.length);
|
|
@@ -1116,6 +1514,12 @@ function statementWrites(statement) {
|
|
|
1116
1514
|
for (const [index, token] of statement.entries()) {
|
|
1117
1515
|
if (isPunct(token, UNCLOSED))
|
|
1118
1516
|
reasons.push("has a quote that never closes");
|
|
1517
|
+
else if (isPunct(token, NUL))
|
|
1518
|
+
reasons.push("contains a NUL byte");
|
|
1519
|
+
else if (isPunct(token, UNICODE_IDENTIFIER))
|
|
1520
|
+
reasons.push("uses a U& escaped identifier");
|
|
1521
|
+
if (token.kind === "quoted" && SIDE_EFFECT_FUNCTIONS.test(token.text))
|
|
1522
|
+
reasons.push(`calls ${token.text}`);
|
|
1119
1523
|
if (token.kind !== "word")
|
|
1120
1524
|
continue;
|
|
1121
1525
|
if (SIDE_EFFECT_FUNCTIONS.test(token.text)) {
|
|
@@ -1148,23 +1552,104 @@ function writes(sql) {
|
|
|
1148
1552
|
var SQL_CLIENTS = new Set(["psql", "mysql", "mariadb", "pgcli", "mycli", "sqlite3"]);
|
|
1149
1553
|
var WRITING_TOOLS = new Set(["dropdb", "createdb", "pg_restore"]);
|
|
1150
1554
|
var CONSOLE_PROGRAMS = new Set(["php", "docker", "symfony", "console", "artisan"]);
|
|
1151
|
-
var
|
|
1152
|
-
var
|
|
1153
|
-
var
|
|
1555
|
+
var VALUE_SHORTS = { psql: "cdfhLopPRTUvF", pgcli: "dhUpW", mysql: "eDhPSuTA", mariadb: "eDhPSuTA", mycli: "eDhPSuTA", sqlite3: "" };
|
|
1556
|
+
var STATEMENT_SHORTS = { psql: "c", pgcli: "c", mysql: "e", mariadb: "e", mycli: "e", sqlite3: "" };
|
|
1557
|
+
var FILE_SHORTS = { psql: "f", pgcli: "", mysql: "", mariadb: "", mycli: "", sqlite3: "" };
|
|
1558
|
+
var IS_MYSQL = new Set(["mysql", "mariadb", "mycli"]);
|
|
1559
|
+
var DATABASE_SHORTS = new Set(["d", "D"]);
|
|
1560
|
+
var STATEMENT_LONGS = new Set(["--command", "--execute"]);
|
|
1561
|
+
var DATABASE_LONGS = new Set(["--dbname", "--database"]);
|
|
1562
|
+
var VALUE_LONGS = new Set(["--host", "--port", "--username", "--user", "--output", "--set", "--variable", "--field-separator", "--record-separator", "--pset", "--log-file", "--socket"]);
|
|
1563
|
+
var PRINTERS2 = new Set(["echo", "printf"]);
|
|
1154
1564
|
var LIST_OPTIONS = new Set(["-l", "--list"]);
|
|
1565
|
+
var REDIRECT_ONLY2 = /^\d*(?:>|>>|>&|&>|>\||<&|<>)$/;
|
|
1566
|
+
var REDIRECT_START2 = /^\d*>|^&>/;
|
|
1567
|
+
var URL_DATABASE = /^[a-z][a-z\d+.-]*:\/\/[^/]*\/([^?#]*)/i;
|
|
1155
1568
|
var WRITING_CONSOLE = /^(?:doctrine:(?:migrations:(?:migrate|execute|rollup)|schema:(?:update|create|drop)|database:(?:create|drop)|fixtures:load)|migrate(?::\w+)?|db:(?:seed|wipe))$/;
|
|
1156
1569
|
var TEST_SCOPED = /(?:^|[^a-z])test/i;
|
|
1157
1570
|
var SHOT_STACK = /\.worktrees\/[^/\s]+\/shot(?:\/|$)|--shot(?:-|$)/;
|
|
1571
|
+
var DEVICE_PATH = /^\/(?:dev|proc|sys)\//;
|
|
1572
|
+
var STDIN_PATHS = new Set(["-", "/dev/stdin"]);
|
|
1158
1573
|
var SQL_WRITE = "Statements that write (INSERT, UPDATE, DELETE, DDL, migrations, fixtures) are handed over: list the exact command for the user to run. Only a clearly test-only database runs them directly.";
|
|
1574
|
+
var SQL_UNREADABLE = "A file this SQL client is fed cannot be read here (missing, a device, or too large), so what it runs is unknown. Hand over the exact command for the user to run.";
|
|
1159
1575
|
function hasWrite(statement) {
|
|
1160
1576
|
return writes(statement).length > 0;
|
|
1161
1577
|
}
|
|
1162
1578
|
function programOf(word) {
|
|
1163
1579
|
return word.split("/").pop() ?? "";
|
|
1164
1580
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1581
|
+
function databaseOf(operand) {
|
|
1582
|
+
return URL_DATABASE.exec(operand)?.[1] ?? operand;
|
|
1583
|
+
}
|
|
1584
|
+
function clientInput(name, args) {
|
|
1585
|
+
const input = { statements: [], files: [], databases: [], operands: [] };
|
|
1586
|
+
const valueShorts = VALUE_SHORTS[name] ?? "";
|
|
1587
|
+
let isOperands = false;
|
|
1588
|
+
for (let i = 0;i < args.length; i++) {
|
|
1589
|
+
const arg = args[i] ?? "";
|
|
1590
|
+
if (isOperands) {
|
|
1591
|
+
input.operands.push(arg);
|
|
1592
|
+
} else if (arg === "--") {
|
|
1593
|
+
isOperands = true;
|
|
1594
|
+
} else if (arg.startsWith("<<<")) {
|
|
1595
|
+
input.statements.push(arg.length > 3 ? arg.slice(3) : args[++i] ?? "");
|
|
1596
|
+
} else if (arg.startsWith("<<")) {
|
|
1597
|
+
continue;
|
|
1598
|
+
} else if (arg === "<") {
|
|
1599
|
+
input.files.push(args[++i] ?? "");
|
|
1600
|
+
} else if (arg.startsWith("<")) {
|
|
1601
|
+
input.files.push(arg.slice(1));
|
|
1602
|
+
} else if (REDIRECT_ONLY2.test(arg)) {
|
|
1603
|
+
i++;
|
|
1604
|
+
} else if (REDIRECT_START2.test(arg)) {
|
|
1605
|
+
continue;
|
|
1606
|
+
} else if (arg.startsWith("--")) {
|
|
1607
|
+
const [option = "", ...rest] = arg.split("=");
|
|
1608
|
+
const attached = rest.length > 0 ? rest.join("=") : undefined;
|
|
1609
|
+
const value = attached ?? args[i + 1] ?? "";
|
|
1610
|
+
if (STATEMENT_LONGS.has(option))
|
|
1611
|
+
input.statements.push(value);
|
|
1612
|
+
else if (option === "--file")
|
|
1613
|
+
input.files.push(value);
|
|
1614
|
+
else if (DATABASE_LONGS.has(option))
|
|
1615
|
+
input.databases.push(databaseOf(value));
|
|
1616
|
+
if (attached === undefined && (STATEMENT_LONGS.has(option) || option === "--file" || DATABASE_LONGS.has(option) || VALUE_LONGS.has(option)))
|
|
1617
|
+
i++;
|
|
1618
|
+
} else if (arg === "-cmd" && name === "sqlite3") {
|
|
1619
|
+
input.statements.push(args[++i] ?? "");
|
|
1620
|
+
} else if (arg.startsWith("-p") && IS_MYSQL.has(name)) {
|
|
1621
|
+
continue;
|
|
1622
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
1623
|
+
for (const [index, letter] of [...arg.slice(1)].entries()) {
|
|
1624
|
+
if (!valueShorts.includes(letter))
|
|
1625
|
+
continue;
|
|
1626
|
+
const attached = arg.slice(index + 2);
|
|
1627
|
+
const value = attached === "" ? args[++i] ?? "" : attached;
|
|
1628
|
+
if (STATEMENT_SHORTS[name]?.includes(letter) === true)
|
|
1629
|
+
input.statements.push(value);
|
|
1630
|
+
else if (FILE_SHORTS[name]?.includes(letter) === true)
|
|
1631
|
+
input.files.push(value);
|
|
1632
|
+
else if (DATABASE_SHORTS.has(letter))
|
|
1633
|
+
input.databases.push(databaseOf(value));
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
} else {
|
|
1637
|
+
input.operands.push(arg);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (name === "sqlite3")
|
|
1641
|
+
input.statements.push(...input.operands.slice(1));
|
|
1642
|
+
else if (input.operands[0] !== undefined)
|
|
1643
|
+
input.databases.push(databaseOf(input.operands[0]));
|
|
1644
|
+
return input;
|
|
1645
|
+
}
|
|
1646
|
+
function isTestScoped(words, client, input) {
|
|
1647
|
+
if (client === -1)
|
|
1648
|
+
return words.some((word) => TEST_SCOPED.test(word) || SHOT_STACK.test(word));
|
|
1649
|
+
return words.slice(0, client).some((word) => TEST_SCOPED.test(word) || SHOT_STACK.test(word)) || (input?.databases ?? []).some((name) => TEST_SCOPED.test(name));
|
|
1650
|
+
}
|
|
1651
|
+
async function fileText(path, read) {
|
|
1652
|
+
return DEVICE_PATH.test(path) ? null : read(path);
|
|
1168
1653
|
}
|
|
1169
1654
|
async function sqlViolation(command, read) {
|
|
1170
1655
|
const pieces = splitCommand(command);
|
|
@@ -1174,23 +1659,27 @@ async function sqlViolation(command, read) {
|
|
|
1174
1659
|
const { program, args } = parseCommand(piece.command);
|
|
1175
1660
|
if (program === "cd")
|
|
1176
1661
|
inShotStack = SHOT_STACK.test(args[0] ?? "");
|
|
1177
|
-
|
|
1662
|
+
const client = words.findIndex((word) => SQL_CLIENTS.has(programOf(word)));
|
|
1663
|
+
const input = client === -1 ? null : clientInput(programOf(words[client] ?? ""), words.slice(client + 1));
|
|
1664
|
+
if (inShotStack || isTestScoped(words, client, input))
|
|
1178
1665
|
continue;
|
|
1179
1666
|
if (WRITING_TOOLS.has(program) && !words.some((word) => LIST_OPTIONS.has(word)))
|
|
1180
1667
|
return SQL_WRITE;
|
|
1181
1668
|
if (CONSOLE_PROGRAMS.has(program) && words.some((word) => WRITING_CONSOLE.test(word)))
|
|
1182
1669
|
return SQL_WRITE;
|
|
1183
|
-
|
|
1184
|
-
if (client === -1)
|
|
1670
|
+
if (input === null)
|
|
1185
1671
|
continue;
|
|
1186
|
-
const files = words.filter((_, i) => i > client && FILE_OPTIONS.has(words[i - 1] ?? ""));
|
|
1187
1672
|
const before = index === 0 ? null : parseCommand(pieces[index - 1]?.command ?? "");
|
|
1673
|
+
const isPiped = pieces[index - 1]?.separator === "|";
|
|
1674
|
+
const paths = input.files.filter((path) => !STDIN_PATHS.has(path));
|
|
1675
|
+
const texts = await Promise.all([...paths, ...isPiped && before?.program === "cat" ? before.args.filter((arg) => !arg.startsWith("-")) : []].map(async (path) => fileText(path, read)));
|
|
1676
|
+
if (texts.includes(null))
|
|
1677
|
+
return SQL_UNREADABLE;
|
|
1188
1678
|
const statements = [
|
|
1189
|
-
...
|
|
1190
|
-
piece.
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
...(await Promise.all(files.map(read))).map((text) => text ?? "")
|
|
1679
|
+
...input.statements,
|
|
1680
|
+
...piece.heredocs,
|
|
1681
|
+
isPiped && before !== null && PRINTERS2.has(before.program) ? before.args.join(" ") : "",
|
|
1682
|
+
...texts.map((text) => text ?? "")
|
|
1194
1683
|
];
|
|
1195
1684
|
if (statements.some(hasWrite))
|
|
1196
1685
|
return SQL_WRITE;
|
|
@@ -1199,17 +1688,49 @@ async function sqlViolation(command, read) {
|
|
|
1199
1688
|
}
|
|
1200
1689
|
|
|
1201
1690
|
// ../env/src/files.ts
|
|
1202
|
-
var ENV_FILE =
|
|
1691
|
+
var ENV_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.envrc)$/;
|
|
1203
1692
|
var TEMPLATE = /\.(?:example|sample|template|dist|defaults)$/;
|
|
1693
|
+
var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
|
|
1204
1694
|
|
|
1205
1695
|
// ../env/src/guard.ts
|
|
1206
1696
|
var REDIRECT = /^\d*[<>]+&?/;
|
|
1697
|
+
var ENVRC = ".envrc";
|
|
1698
|
+
var PROC_ENVIRON = /^\/proc\/[^/]+\/environ$/;
|
|
1699
|
+
var ENV_NAME_IN_CODE = /(?:^|[^\w.-])(\.env(?:\.[\w.-]*\w)?|\.envrc)(?![\w-])/g;
|
|
1700
|
+
var PYTHON_ENVIRON = /\bos\.environ\b(?!\s*(?:\[|\.get\b))/;
|
|
1701
|
+
var VARIABLE = /\$(?:(\w+)|\{(\w+)\})/;
|
|
1702
|
+
var ASSIGNMENT2 = /^\w+=/;
|
|
1703
|
+
var SHORT_FLAGS = /^-[a-z]+$/i;
|
|
1704
|
+
var OPENER = /^[({]+/;
|
|
1705
|
+
var ENV_SAMPLES = [".env", ".env.local", ".env.production", ".envrc"];
|
|
1706
|
+
var MAX_VARIANTS = 64;
|
|
1707
|
+
var INTERPRETERS = new Set(["bun", "deno", "node", "python", "python3", "ruby", "perl", "php", "lua", "awk", "gawk"]);
|
|
1708
|
+
var CODE_OPTIONS = new Set(["-e", "-c", "-p", "-E", "-pe", "-r", "--eval", "--print", "--command"]);
|
|
1709
|
+
var GREPS = new Set(["grep", "egrep", "fgrep"]);
|
|
1710
|
+
var GREP_VALUE_OPTIONS = new Set(["-e", "-f", "-m", "-A", "-B", "-C", "-d", "-D", "--regexp", "--file", "--max-count", "--include", "--exclude", "--exclude-dir", "--exclude-from", "--context", "--after-context", "--before-context", "--label", "--binary-files", "--directories", "--devices"]);
|
|
1711
|
+
var RG_VALUE_OPTIONS = new Set(["-e", "-f", "-g", "-t", "-T", "-m", "-A", "-B", "-C", "-j", "-d", "--regexp", "--file", "--glob", "--iglob", "--type", "--type-not", "--max-count", "--context", "--after-context", "--before-context", "--threads", "--max-depth", "--max-filesize"]);
|
|
1712
|
+
var PATTERN_OPTIONS = new Set(["-e", "-f", "--regexp", "--file"]);
|
|
1713
|
+
var HIDDEN_FLAGS = /^(?:-u{2,}|--hidden|-\.)$/i;
|
|
1714
|
+
var REGEX_SPECIALS = /[.+^${}()|\\[\]]/g;
|
|
1715
|
+
var NEGATED_CLASS = /^!/;
|
|
1716
|
+
var GLOB_CHAR = /[*?[]/;
|
|
1717
|
+
var HAS_R = /r/i;
|
|
1718
|
+
var DUMP_FLAGS = /^-[a-z]*[px]/;
|
|
1719
|
+
var DECLARERS = new Set(["export", "declare", "local", "readonly", "typeset"]);
|
|
1720
|
+
var WHOLE_TREE_OPERAND = /^(?:(?:\.\.?\/?)+|~\/?|\$HOME\/?|\/)$/;
|
|
1721
|
+
var GIT_PATCHES = new Set(["diff", "log", "show", "whatchanged", "format-patch", "grep"]);
|
|
1722
|
+
var GIT_NO_PATCH = new Set(["--stat", "--numstat", "--shortstat", "--name-only", "--name-status", "--summary", "-s", "--no-patch", "--check", "--quiet"]);
|
|
1723
|
+
var GIT_VALUE_OPTIONS = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path"]);
|
|
1724
|
+
var GIT_PATCH_FLAGS = new Set(["-p", "-u", "--patch", "-c", "--cc", "--patch-with-stat", "--patch-with-raw"]);
|
|
1725
|
+
var GIT_EXCLUDE = /^:(?:!|\^|\(exclude\))/;
|
|
1726
|
+
var EXCLUDES_ENV = /^(?:--exclude(?:-dir)?=.*env|(?:--glob=|--iglob=|-g)?!.*env)/;
|
|
1727
|
+
var EXCLUDE_OPTIONS = new Set(["--exclude", "--exclude-dir", "-g", "--glob", "--iglob"]);
|
|
1207
1728
|
var ENV_DUMP = /(?:console\.\w+|JSON\.stringify|Object\.(?:entries|keys|values)|print)\(\s*process\.env\s*[,)]/;
|
|
1208
1729
|
var HARMLESS = new Set(["ev", "redact", "ls", "stat", "test", "[", "[[", "touch"]);
|
|
1209
1730
|
var GIT_HARMLESS = new Set(["check-ignore", "ls-files", "status"]);
|
|
1210
1731
|
var ENV_PRINTERS = new Set(["env", "printenv"]);
|
|
1211
1732
|
var DOCKER = new Set(["docker", "docker-compose"]);
|
|
1212
|
-
var
|
|
1733
|
+
var SHELLS2 = new Set(["sh", "bash", "zsh", "ash"]);
|
|
1213
1734
|
var DOCKER_VALUE_OPTIONS = new Set(["-f", "--file", "-p", "--project-name", "--profile", "--env-file", "--project-directory", "--ansi", "--progress", "--parallel", "-H", "--host", "-c", "--context", "--config", "-l", "--log-level"]);
|
|
1214
1735
|
var EXEC_VALUE_OPTIONS = new Set(["-e", "--env", "--env-file", "-u", "--user", "-w", "--workdir", "--index", "--detach-keys"]);
|
|
1215
1736
|
var CONFIG_LISTS = new Set(["--services", "--volumes", "--profiles", "--images", "--networks", "--hash", "-q", "--quiet"]);
|
|
@@ -1218,13 +1739,79 @@ var WHOLE_FORMAT = /Env|\{\{\s*json\s+\.\s*\}\}|^json$/;
|
|
|
1218
1739
|
var USE_EV = '.env files hold secrets that must never reach the transcript, so they are not read, edited or printed directly. Use `ev` instead: `ev list`, `ev get KEY` (masked), `ev grep <regex>` for the keys matching it, `ev show <rev>:<file>` for one at a git revision, `printf %s "$value" | ev set KEY [file]` (`--empty` for an empty value), `ev unset KEY`, `ev cp KEY --from .env --to .env.local`, `ev mv KEY --from .env --to .env.local`, `ev diff a b`, `ev missing`; and pipe output that may carry a secret through `redact`, as `docker compose exec <svc> printenv | redact` or `docker compose config | redact`. If `ev` is missing: `bun install -g @kaynooo/env-tools`.';
|
|
1219
1740
|
function isEnvFile(path) {
|
|
1220
1741
|
const name = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf(":")) + 1);
|
|
1221
|
-
return (ENV_FILE.test(name) || name === ".env*") && !TEMPLATE.test(name);
|
|
1742
|
+
return (ENV_FILE.test(name) || name === ".env*" || name === ENVRC) && !TEMPLATE.test(name);
|
|
1743
|
+
}
|
|
1744
|
+
function globRegex(glob) {
|
|
1745
|
+
let source = "";
|
|
1746
|
+
for (let i = 0;i < glob.length; i++) {
|
|
1747
|
+
const char = glob[i] ?? "";
|
|
1748
|
+
const close = char === "[" ? glob.indexOf("]", i + 2) : -1;
|
|
1749
|
+
if (char === "*") {
|
|
1750
|
+
source += ".*";
|
|
1751
|
+
} else if (char === "?") {
|
|
1752
|
+
source += ".";
|
|
1753
|
+
} else if (close === -1) {
|
|
1754
|
+
source += char.replace(REGEX_SPECIALS, String.raw`\$&`);
|
|
1755
|
+
} else {
|
|
1756
|
+
source += `[${glob.slice(i + 1, close).replace(NEGATED_CLASS, "^")}]`;
|
|
1757
|
+
i = close;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return new RegExp(`^${source}$`);
|
|
1222
1761
|
}
|
|
1223
|
-
function
|
|
1224
|
-
|
|
1762
|
+
function globReachesEnv(path) {
|
|
1763
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
1764
|
+
if (!name.startsWith(".") || !GLOB_CHAR.test(name))
|
|
1225
1765
|
return false;
|
|
1766
|
+
const regex = globRegex(name);
|
|
1767
|
+
return ENV_SAMPLES.some((sample) => regex.test(sample));
|
|
1768
|
+
}
|
|
1769
|
+
function reachesEnvFile(path) {
|
|
1770
|
+
return braceExpand(path).some((each) => isEnvFile(each) || globReachesEnv(each));
|
|
1771
|
+
}
|
|
1772
|
+
function targetOf(word) {
|
|
1226
1773
|
const bare = word.replace(REDIRECT, "");
|
|
1227
|
-
return
|
|
1774
|
+
return bare.includes("=") ? bare.slice(bare.indexOf("=") + 1) : bare;
|
|
1775
|
+
}
|
|
1776
|
+
function namesEnvFile(word, previous, isGlob) {
|
|
1777
|
+
if (previous === "--env-file" || word.startsWith("--env-file=") || word.startsWith("--exclude") || EXCLUDE_OPTIONS.has(previous ?? ""))
|
|
1778
|
+
return false;
|
|
1779
|
+
const path = targetOf(word);
|
|
1780
|
+
return isEnvFile(path) || PROC_ENVIRON.test(path) || isGlob && globReachesEnv(path);
|
|
1781
|
+
}
|
|
1782
|
+
function variablesOf(command) {
|
|
1783
|
+
const variables = new Map;
|
|
1784
|
+
const add = (assignment) => {
|
|
1785
|
+
const at = assignment.indexOf("=");
|
|
1786
|
+
const name = assignment.slice(0, at);
|
|
1787
|
+
variables.set(name, [...variables.get(name) ?? [], assignment.slice(at + 1)]);
|
|
1788
|
+
};
|
|
1789
|
+
for (const piece of splitCommand(command)) {
|
|
1790
|
+
const { program, args, assignments } = parseCommand(piece.command);
|
|
1791
|
+
const tokens = tokenize(piece.command);
|
|
1792
|
+
const declared = DECLARERS.has(program) ? args.filter((arg) => ASSIGNMENT2.test(arg)) : [];
|
|
1793
|
+
for (const assignment of [...assignments, ...declared])
|
|
1794
|
+
add(assignment);
|
|
1795
|
+
if (tokens[0] === "for" && tokens[2] === "in" && tokens[1] !== undefined)
|
|
1796
|
+
variables.set(tokens[1], [...variables.get(tokens[1]) ?? [], ...tokens.slice(3)]);
|
|
1797
|
+
}
|
|
1798
|
+
return variables;
|
|
1799
|
+
}
|
|
1800
|
+
function variantsOf(word, variables) {
|
|
1801
|
+
let found = [word.text];
|
|
1802
|
+
for (let round = 0;round < 4 && found.some((each) => VARIABLE.test(each)); round++) {
|
|
1803
|
+
found = found.flatMap((each) => {
|
|
1804
|
+
const match = VARIABLE.exec(each);
|
|
1805
|
+
const values = variables.get(match?.[1] ?? match?.[2] ?? "");
|
|
1806
|
+
if (match === null || values === undefined)
|
|
1807
|
+
return [each.replace(VARIABLE, "")];
|
|
1808
|
+
return values.map((value) => each.slice(0, match.index) + value + each.slice(match.index + match[0].length));
|
|
1809
|
+
}).slice(0, MAX_VARIANTS);
|
|
1810
|
+
}
|
|
1811
|
+
return found.flatMap((each) => word.hasBrace ? braceExpand(each) : [each]);
|
|
1812
|
+
}
|
|
1813
|
+
function codeReadsEnv(found) {
|
|
1814
|
+
return found.some((word, i) => (word.isProse || CODE_OPTIONS.has(found[i - 1]?.text ?? "")) && (PYTHON_ENVIRON.test(word.text) || [...word.text.matchAll(ENV_NAME_IN_CODE)].some((match) => isEnvFile(match[1] ?? ""))));
|
|
1228
1815
|
}
|
|
1229
1816
|
function firstOperand(words, valueOptions) {
|
|
1230
1817
|
for (let i = 0;i < words.length; i++) {
|
|
@@ -1237,7 +1824,7 @@ function firstOperand(words, valueOptions) {
|
|
|
1237
1824
|
return words.length;
|
|
1238
1825
|
}
|
|
1239
1826
|
function printsEnv([program = "", ...args]) {
|
|
1240
|
-
if (
|
|
1827
|
+
if (SHELLS2.has(program) && args[0] === "-c")
|
|
1241
1828
|
return splitCommand(args[1] ?? "").some((piece) => ENV_PRINTERS.has(tokenize(piece.command)[0] ?? ""));
|
|
1242
1829
|
return ENV_PRINTERS.has(program);
|
|
1243
1830
|
}
|
|
@@ -1259,24 +1846,79 @@ function dumpsContainerEnv(program, args) {
|
|
|
1259
1846
|
const format = after.find((_, i) => FORMAT_OPTIONS.has(after[i - 1] ?? "")) ?? after.find((word) => word.startsWith("--format="))?.slice("--format=".length);
|
|
1260
1847
|
return format === undefined || WHOLE_FORMAT.test(format);
|
|
1261
1848
|
}
|
|
1262
|
-
function readsEnv(piece) {
|
|
1849
|
+
function readsEnv(piece, variables) {
|
|
1263
1850
|
const { program, args } = parseCommand(piece.command);
|
|
1264
1851
|
const found = words(piece.command);
|
|
1265
1852
|
if (program === "" || HARMLESS.has(program) || program === "git" && GIT_HARMLESS.has(args[0] ?? ""))
|
|
1266
1853
|
return false;
|
|
1267
|
-
|
|
1854
|
+
if (INTERPRETERS.has(program) && codeReadsEnv(found))
|
|
1855
|
+
return true;
|
|
1856
|
+
const isExclusion = (word) => program === "git" && GIT_EXCLUDE.test(word.text);
|
|
1857
|
+
return found.some((word, index) => !word.isProse && !isExclusion(word) && variantsOf(word, variables).some((variant) => namesEnvFile(variant, found[index - 1]?.text, word.hasGlob)));
|
|
1268
1858
|
}
|
|
1269
|
-
function
|
|
1859
|
+
function operandsOf(args, valueOptions) {
|
|
1860
|
+
const operands = [];
|
|
1861
|
+
let hasPattern = false;
|
|
1862
|
+
for (let i = 0;i < args.length; i++) {
|
|
1863
|
+
const arg = args[i] ?? "";
|
|
1864
|
+
if (PATTERN_OPTIONS.has(arg) || arg.startsWith("--regexp=") || arg.startsWith("--file="))
|
|
1865
|
+
hasPattern = true;
|
|
1866
|
+
if (valueOptions.has(arg))
|
|
1867
|
+
i++;
|
|
1868
|
+
else if (!arg.startsWith("-"))
|
|
1869
|
+
operands.push(arg);
|
|
1870
|
+
}
|
|
1871
|
+
return hasPattern ? operands : operands.slice(1);
|
|
1872
|
+
}
|
|
1873
|
+
function searchesEnv(program, args) {
|
|
1874
|
+
const isGrep = GREPS.has(program);
|
|
1875
|
+
if (!isGrep && program !== "rg" && program !== "ag")
|
|
1876
|
+
return false;
|
|
1877
|
+
const isRecursive = args.some((arg) => SHORT_FLAGS.test(arg) && HAS_R.test(arg) || arg === "--recursive" || arg === "--dereference-recursive");
|
|
1878
|
+
const isHidden = args.some((arg) => HIDDEN_FLAGS.test(arg) || program === "ag" && (arg === "-u" || arg === "--unrestricted"));
|
|
1879
|
+
const isLimited = args.some((arg, i) => arg.startsWith("--include") || EXCLUDES_ENV.test(arg) || EXCLUDE_OPTIONS.has(args[i - 1] ?? "") && arg.includes("env"));
|
|
1880
|
+
if (isLimited || (isGrep ? !isRecursive : !isHidden))
|
|
1881
|
+
return false;
|
|
1882
|
+
const operands = operandsOf(args, isGrep ? GREP_VALUE_OPTIONS : RG_VALUE_OPTIONS);
|
|
1883
|
+
return operands.length === 0 || operands.some((operand) => WHOLE_TREE_OPERAND.test(operand));
|
|
1884
|
+
}
|
|
1885
|
+
function dumpsEnvironment(program, args, tokens) {
|
|
1886
|
+
const isBare = args.every((arg) => arg.startsWith("-"));
|
|
1887
|
+
if (program === "printenv" || program === "export")
|
|
1888
|
+
return isBare;
|
|
1889
|
+
if (program === "declare" || program === "typeset")
|
|
1890
|
+
return isBare && args.some((arg) => DUMP_FLAGS.test(arg));
|
|
1891
|
+
if (program === "set")
|
|
1892
|
+
return args.length === 0;
|
|
1893
|
+
return program === "" && tokens.some((token) => token.replace(OPENER, "") === "env");
|
|
1894
|
+
}
|
|
1895
|
+
function printsTrackedEnv(program, args) {
|
|
1896
|
+
if (program !== "git")
|
|
1897
|
+
return false;
|
|
1898
|
+
const at = args.findIndex((arg, i) => !arg.startsWith("-") && !GIT_VALUE_OPTIONS.has(args[i - 1] ?? ""));
|
|
1899
|
+
const name = args[at] ?? "";
|
|
1900
|
+
const rest = args.slice(at + 1);
|
|
1901
|
+
if (!GIT_PATCHES.has(name) || rest.some((arg) => GIT_EXCLUDE.test(arg)))
|
|
1902
|
+
return false;
|
|
1903
|
+
if (name === "grep")
|
|
1904
|
+
return true;
|
|
1905
|
+
if (name === "log" || name === "whatchanged")
|
|
1906
|
+
return rest.some((arg) => GIT_PATCH_FLAGS.has(arg));
|
|
1907
|
+
return !rest.some((arg) => GIT_NO_PATCH.has(arg));
|
|
1908
|
+
}
|
|
1909
|
+
function touchesEnv(command, options = {}) {
|
|
1270
1910
|
if (ENV_DUMP.test(command))
|
|
1271
1911
|
return true;
|
|
1272
|
-
if (substitutions(command).some(touchesEnv))
|
|
1912
|
+
if (substitutions(command).some((each) => touchesEnv(each, options)))
|
|
1273
1913
|
return true;
|
|
1274
1914
|
const pieces = splitCommand(command);
|
|
1915
|
+
const variables = variablesOf(command);
|
|
1275
1916
|
return pieces.some((piece, index) => {
|
|
1276
|
-
if (readsEnv(piece))
|
|
1277
|
-
return true;
|
|
1278
1917
|
const { program, args } = parseCommand(piece.command);
|
|
1279
|
-
if (
|
|
1918
|
+
if (readsEnv(piece, variables) || searchesEnv(program, args))
|
|
1919
|
+
return true;
|
|
1920
|
+
const isDump = dumpsContainerEnv(program, args) || dumpsEnvironment(program, args, tokenize(piece.command)) || options.hasTrackedEnv === true && printsTrackedEnv(program, args);
|
|
1921
|
+
if (!isDump)
|
|
1280
1922
|
return false;
|
|
1281
1923
|
for (let at = index;pieces[at]?.separator === "|"; at++) {
|
|
1282
1924
|
if (parseCommand(pieces[at + 1]?.command ?? "").program === "redact")
|
|
@@ -1287,17 +1929,19 @@ function touchesEnv(command) {
|
|
|
1287
1929
|
}
|
|
1288
1930
|
|
|
1289
1931
|
// ../git/src/message.ts
|
|
1290
|
-
var CONVENTIONAL = /^
|
|
1932
|
+
var CONVENTIONAL = /^(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\([^()\n]+\))?!?: \S/;
|
|
1933
|
+
var MAX_TITLE = 100;
|
|
1291
1934
|
var GIT_MADE = /^(?:Merge |Revert "|fixup! |squash! |amend! )/;
|
|
1292
1935
|
var TICKET = /^(?:[\w.-]+\/[\w.-]+)?#\d+ - /;
|
|
1293
1936
|
var BREAKING = /^BREAKING[ -]CHANGE: /;
|
|
1294
1937
|
var BREAKING_TITLE = /^[a-z]+(?:\([^()\n]+\))?!: /;
|
|
1295
|
-
var TRAILER = /^(?:co-authored-by|signed-off-by|reviewed-by|acked-by|tested-by|reported-by|suggested-by|helped-by|change-id):|^\W*generated with\
|
|
1938
|
+
var TRAILER = /^(?:co-authored-by|signed-off-by|reviewed-by|acked-by|tested-by|reported-by|suggested-by|helped-by|change-id):|^\W*generated with \W*claude/i;
|
|
1296
1939
|
var BULLET = /^\s*(?:[-*+]|\d+[.)])\s/;
|
|
1297
1940
|
var SENTENCE_END = /[.!?:;)\]`'"]$/;
|
|
1298
1941
|
var LOWER_START = /^\p{Ll}/u;
|
|
1299
1942
|
var RULE = 'The rule: a `type(scope): title` line, `!` after the scope for a breaking change, type and scope taken from the repository\'s history; then optionally a blank line and one or two lines on what changed, never a bullet list or a hard-wrapped sentence; then a blank line and one line per ticket, its title copied verbatim, `#<n> - <title>` (`owner/repo#<n> - <title>` for another repository). The message ends there: no Co-Authored-By, Signed-off-by or "Generated with" trailer. Only a breaking change gives full details.';
|
|
1300
1943
|
var TITLE = `The title is no Conventional Commit. ${RULE}`;
|
|
1944
|
+
var TITLE_TOO_LONG = `The title runs past ${MAX_TITLE} characters: shorten it and move the detail to the body. ${RULE}`;
|
|
1301
1945
|
var TITLE_LINE = `The title takes one line and a blank line follows it. ${RULE}`;
|
|
1302
1946
|
var TRAILERS = `No trailers, ever: no Co-Authored-By, Signed-off-by or "Generated with" line, no --signoff or --trailer. ${RULE}`;
|
|
1303
1947
|
var BULLETS = `The body names what changed in a line or two, never a bullet list narrating the diff. ${RULE}`;
|
|
@@ -1323,20 +1967,26 @@ function commitMessageViolation(message) {
|
|
|
1323
1967
|
return TITLE;
|
|
1324
1968
|
if (second !== undefined && second !== "")
|
|
1325
1969
|
return TITLE_LINE;
|
|
1326
|
-
if (
|
|
1970
|
+
if (!GIT_MADE.test(title) && title.length > MAX_TITLE)
|
|
1971
|
+
return TITLE_TOO_LONG;
|
|
1972
|
+
if (BREAKING_TITLE.test(title))
|
|
1327
1973
|
return null;
|
|
1328
|
-
const
|
|
1974
|
+
const footer = body.findIndex((line) => BREAKING.test(line));
|
|
1975
|
+
const described = footer === -1 ? body : body.slice(0, footer);
|
|
1976
|
+
const descriptions = described.filter(isDescription);
|
|
1329
1977
|
if (descriptions.some((line) => BULLET.test(line)))
|
|
1330
1978
|
return BULLETS;
|
|
1331
1979
|
if (descriptions.length > 2)
|
|
1332
1980
|
return TOO_LONG;
|
|
1333
|
-
const isWrapped =
|
|
1981
|
+
const isWrapped = described.some((line, i) => isDescription(line) && isDescription(described[i + 1]) && !SENTENCE_END.test(line) && LOWER_START.test(described[i + 1] ?? ""));
|
|
1334
1982
|
return isWrapped ? WRAPPED : null;
|
|
1335
1983
|
}
|
|
1336
1984
|
|
|
1337
1985
|
// ../out/src/wrap.ts
|
|
1338
|
-
var
|
|
1339
|
-
var EXCLUDED = /(?:^|\s)(?:--json|--help|--version|-h)(?:\s|$)/;
|
|
1986
|
+
var ASSIGNMENT3 = /^\w+=/;
|
|
1987
|
+
var EXCLUDED = /(?:^|\s)(?:--json|--help|--version|-h|--watch(?:-?all|=\S*)?|--watchAll)(?:\s|$)/;
|
|
1988
|
+
var SHORT_WATCH = /(?:^|\s)(?:\S*\/)?(?:tsc|tsgo|vue-tsc|vitest|jest)\s(?:.*\s)?-w(?:\s|$)/;
|
|
1989
|
+
var LONG_RUNNING = /^(?:dev|watch|serve|start|up|run|preview|storybook)(?:[:-]\S*)?$/;
|
|
1340
1990
|
var BUN_SCRIPT = /^(?:build|check|lint|typecheck|types|test|ci|verify|compile)(?::|$)/;
|
|
1341
1991
|
var BUN_TOOLS = new Set(["tsc", "tsgo", "eslint", "keslint", "vitest", "jest", "prettier", "playwright", "biome", "vue-tsc"]);
|
|
1342
1992
|
var BUN_SUBCOMMANDS = new Set(["install", "i", "add", "remove", "rm", "update", "test", "check", "lint", "build"]);
|
|
@@ -1372,9 +2022,9 @@ var RULES = {
|
|
|
1372
2022
|
"php-cs-fixer": always,
|
|
1373
2023
|
rector: always,
|
|
1374
2024
|
php: (args) => listed(afterOptions(args, PHP_VALUE_OPTIONS)),
|
|
1375
|
-
make:
|
|
2025
|
+
make: ([first]) => first === undefined || !LONG_RUNNING.test(first),
|
|
1376
2026
|
check: always,
|
|
1377
|
-
just: ([first]) => first !== undefined && !first.startsWith("-"),
|
|
2027
|
+
just: ([first]) => first !== undefined && !first.startsWith("-") && !LONG_RUNNING.test(first),
|
|
1378
2028
|
composer: ([sub]) => sub === "install" || sub === "update" || sub === "require",
|
|
1379
2029
|
pip: ([sub]) => sub === "install",
|
|
1380
2030
|
cargo: ([sub]) => ["build", "test", "check", "clippy"].includes(sub ?? ""),
|
|
@@ -1432,14 +2082,14 @@ function withoutFilters(command) {
|
|
|
1432
2082
|
return rest;
|
|
1433
2083
|
}
|
|
1434
2084
|
function listed(words) {
|
|
1435
|
-
const start = words.findIndex((word) => !
|
|
2085
|
+
const start = words.findIndex((word) => !ASSIGNMENT3.test(word));
|
|
1436
2086
|
const path = words[start];
|
|
1437
2087
|
if (path === undefined)
|
|
1438
2088
|
return false;
|
|
1439
2089
|
return RULES[path.slice(path.lastIndexOf("/") + 1)]?.(words.slice(start + 1)) ?? false;
|
|
1440
2090
|
}
|
|
1441
2091
|
function listedSegment(segment) {
|
|
1442
|
-
return !EXCLUDED.test(segment) && listed(tokenize(segment));
|
|
2092
|
+
return !EXCLUDED.test(segment) && !SHORT_WATCH.test(segment) && listed(tokenize(segment));
|
|
1443
2093
|
}
|
|
1444
2094
|
function isCd(segment) {
|
|
1445
2095
|
return segment === "cd" || segment.startsWith("cd ");
|
|
@@ -1450,14 +2100,14 @@ function shouldWrap(command) {
|
|
|
1450
2100
|
return false;
|
|
1451
2101
|
return segments.every((segment) => isCd(segment) || listedSegment(segment));
|
|
1452
2102
|
}
|
|
1453
|
-
function
|
|
2103
|
+
function quoted2(value) {
|
|
1454
2104
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1455
2105
|
}
|
|
1456
2106
|
function wrap(command) {
|
|
1457
2107
|
const trimmed = withoutFilters(command.trim());
|
|
1458
|
-
if (scan(trimmed).segments.length <= 1 && !
|
|
2108
|
+
if (scan(trimmed).segments.length <= 1 && !ASSIGNMENT3.test(trimmed))
|
|
1459
2109
|
return `out -- ${trimmed}`;
|
|
1460
|
-
return `out -- sh -c ${
|
|
2110
|
+
return `out -- sh -c ${quoted2(trimmed)}`;
|
|
1461
2111
|
}
|
|
1462
2112
|
|
|
1463
2113
|
// ../pkg/src/added.ts
|
|
@@ -1466,10 +2116,18 @@ var BARE_REDIRECT = /^\d*(?:[<>]|>>)$/;
|
|
|
1466
2116
|
var ADDS = {
|
|
1467
2117
|
bun: new Set(["add", "a", "install", "i"]),
|
|
1468
2118
|
npm: new Set(["install", "i", "add", "in", "isntall"]),
|
|
1469
|
-
pnpm: new Set(["add"]),
|
|
2119
|
+
pnpm: new Set(["add", "i", "install"]),
|
|
1470
2120
|
yarn: new Set(["add"])
|
|
1471
2121
|
};
|
|
1472
|
-
var
|
|
2122
|
+
var RUNS = {
|
|
2123
|
+
bun: new Set(["x"]),
|
|
2124
|
+
npm: new Set(["exec", "x"]),
|
|
2125
|
+
pnpm: new Set(["dlx"]),
|
|
2126
|
+
yarn: new Set(["dlx"])
|
|
2127
|
+
};
|
|
2128
|
+
var RUNNERS = new Set(["bunx", "npx"]);
|
|
2129
|
+
var PACKAGE_FLAGS = new Set(["-p", "--package"]);
|
|
2130
|
+
var VALUE_FLAGS = new Set(["--cwd", "--filter", "-F", "--workspace", "--registry", "--prefix", "-C", "--dir", "--config", "--cache-dir", "--loglevel", "--tag", "--save-prefix", "--userconfig", "--omit", "--include", "--fetch-timeout", "--fetch-retries", "--install-strategy", "--cache", ...PACKAGE_FLAGS]);
|
|
1473
2131
|
var NOT_REGISTRY = /^(?:[./~]|file:|link:|portal:|workspace:|git[+:]|github:|gitlab:|bitbucket:|https?:)|\.tgz$|^[\w.-]+\/[\w.-]+(?:#.*)?$/;
|
|
1474
2132
|
var NPM_ALIAS = /^(?:@[^/]+\/)?[^@]+@npm:(.+)$/;
|
|
1475
2133
|
var PUBLISH = /\b(?:ft|cli\.ts)\s+release\b|\b(?:bun|npm|pnpm|yarn)\s+publish\b|\bjust\s+(?:patch|minor|major)\b/;
|
|
@@ -1479,16 +2137,38 @@ function addedPackages(command) {
|
|
|
1479
2137
|
published ||= PUBLISH.test(piece.command);
|
|
1480
2138
|
const { program, args } = parseCommand(piece.command);
|
|
1481
2139
|
const [sub = "", ...rest] = args;
|
|
1482
|
-
if (published
|
|
2140
|
+
if (published)
|
|
2141
|
+
return [];
|
|
2142
|
+
if (RUNNERS.has(program))
|
|
2143
|
+
return runnerPackages(args);
|
|
2144
|
+
if (RUNS[program]?.has(sub))
|
|
2145
|
+
return runnerPackages(rest);
|
|
2146
|
+
if (!ADDS[program]?.has(sub))
|
|
1483
2147
|
return [];
|
|
1484
|
-
return rest.flatMap((word, index) =>
|
|
1485
|
-
const previous = rest[index - 1] ?? "";
|
|
1486
|
-
if (word.startsWith("-") || REDIRECT2.test(word) || VALUE_FLAGS.has(previous) || BARE_REDIRECT.test(previous) || NOT_REGISTRY.test(word))
|
|
1487
|
-
return [];
|
|
1488
|
-
return [NPM_ALIAS.exec(word)?.[1] ?? word];
|
|
1489
|
-
});
|
|
2148
|
+
return rest.flatMap((word, index) => isSkipped(word, rest[index - 1] ?? "") ? [] : registryName(word));
|
|
1490
2149
|
});
|
|
1491
2150
|
}
|
|
2151
|
+
function isSkipped(word, previous) {
|
|
2152
|
+
return word.startsWith("-") || REDIRECT2.test(word) || VALUE_FLAGS.has(previous) || BARE_REDIRECT.test(previous);
|
|
2153
|
+
}
|
|
2154
|
+
function registryName(word) {
|
|
2155
|
+
return NOT_REGISTRY.test(word) ? [] : [NPM_ALIAS.exec(word)?.[1] ?? word];
|
|
2156
|
+
}
|
|
2157
|
+
function runnerPackages(words) {
|
|
2158
|
+
const named = [];
|
|
2159
|
+
for (const [index, word] of words.entries()) {
|
|
2160
|
+
const previous = words[index - 1] ?? "";
|
|
2161
|
+
if (PACKAGE_FLAGS.has(previous))
|
|
2162
|
+
named.push(word);
|
|
2163
|
+
else if (word.startsWith("--package="))
|
|
2164
|
+
named.push(word.slice("--package=".length));
|
|
2165
|
+
else if (!isSkipped(word, previous) && named.length === 0)
|
|
2166
|
+
return registryName(word);
|
|
2167
|
+
else if (!isSkipped(word, previous))
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
return named.flatMap(registryName);
|
|
2171
|
+
}
|
|
1492
2172
|
|
|
1493
2173
|
// ../pkg/src/pin.ts
|
|
1494
2174
|
var LOCKFILES = [
|
|
@@ -1617,11 +2297,16 @@ var COMMIT_VALUE_LONGS = new Set(["--message", "--file", "--reuse-message", "--r
|
|
|
1617
2297
|
var SHORT_CLUSTER = /^-[A-Z]+$/i;
|
|
1618
2298
|
var STAGES_ALL = /[Au]/;
|
|
1619
2299
|
var ALL_TAGS = new Set(["--tags", "--follow-tags"]);
|
|
2300
|
+
var TOP_MAGIC = /^:(?:\/|\([^)]*\))/;
|
|
2301
|
+
var RISKY_CONFIG = /^(?:alias\.|core\.(?:sshcommand|hookspath|pager|editor|fsmonitor|askpass|gitproxy)|credential\.|diff\.external|sequence\.editor|pager\.|filter\.|gpg\..*program)/i;
|
|
2302
|
+
var RISKY_ENV = /^GIT_(?:CONFIG_(?:COUNT|PARAMETERS)|EXTERNAL_DIFF|ASKPASS|PROXY_COMMAND|PAGER)=/;
|
|
2303
|
+
var READS_CONFIG = new Set(["--get", "--get-all", "--get-regexp", "--list", "-l", "--unset", "--unset-all", "--show-origin", "--show-scope"]);
|
|
1620
2304
|
var STAGE_BY_PATH = `Stage by path: the tree carries unrelated work and peer sessions edit it live. ${USE_GC}`;
|
|
1621
2305
|
var STASH = "Never `git stash`: a pop with nothing saved applies someone else's old stash. To prove a test fails without your change, `proof --keep <test> -- <cmd>` runs it at HEAD in a temporary worktree and again here; `base -- <cmd>` runs any command at HEAD (`--ref <rev>` another commit), the working tree untouched. To set edits aside before a cherry-pick, a rebase or a checkout, commit them with `gc` first, or do the other work in `git worktree add`.";
|
|
1622
2306
|
var ADD_ALL = `\`git add\` over the whole tree is refused. ${STAGE_BY_PATH}`;
|
|
1623
2307
|
var COMMIT_ALL = `\`git commit -a\` stages the whole tree and is refused. ${STAGE_BY_PATH}`;
|
|
1624
2308
|
var PUSH = "`git push` runs only when the user asked for a push in their latest prompt: say the commit is ready and let them ask.";
|
|
2309
|
+
var CONFIG_OVERRIDE = "Never set git config that runs a program or defines a command (`-c alias.*`, `--config-env`, `core.sshCommand`, `core.hooksPath`, `core.pager`, credential helpers, `git config alias.*`): it sidesteps every rule here. Run the git command itself.";
|
|
1625
2310
|
var TAGS_APART = "A tag goes out with its branch in one `git push --atomic origin <branch> <tag>`, never a separate `git push --tags`.";
|
|
1626
2311
|
function subcommandOf(args) {
|
|
1627
2312
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -1633,6 +2318,35 @@ function subcommandOf(args) {
|
|
|
1633
2318
|
}
|
|
1634
2319
|
return { name: "", rest: [] };
|
|
1635
2320
|
}
|
|
2321
|
+
function overridesConfig(args) {
|
|
2322
|
+
for (let i = 0;i < args.length; i++) {
|
|
2323
|
+
const arg = args[i] ?? "";
|
|
2324
|
+
if (arg === "--config-env" || arg.startsWith("--config-env="))
|
|
2325
|
+
return true;
|
|
2326
|
+
if (arg === "-c" && RISKY_CONFIG.test(args[i + 1] ?? ""))
|
|
2327
|
+
return true;
|
|
2328
|
+
if (GLOBAL_VALUE_OPTIONS.has(arg))
|
|
2329
|
+
i++;
|
|
2330
|
+
else if (!arg.startsWith("-"))
|
|
2331
|
+
return false;
|
|
2332
|
+
}
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
function isWholeTree(arg) {
|
|
2336
|
+
if (WHOLE_TREE.has(arg))
|
|
2337
|
+
return true;
|
|
2338
|
+
if (arg.startsWith("-"))
|
|
2339
|
+
return SHORT_CLUSTER.test(arg) && STAGES_ALL.test(arg);
|
|
2340
|
+
const parts = [];
|
|
2341
|
+
for (const part of arg.replace(TOP_MAGIC, "").split("/")) {
|
|
2342
|
+
if (part === ".." && parts.length > 0 && parts.at(-1) !== "..")
|
|
2343
|
+
parts.pop();
|
|
2344
|
+
else if (part !== "" && part !== ".")
|
|
2345
|
+
parts.push(part);
|
|
2346
|
+
}
|
|
2347
|
+
const path = parts.join("/");
|
|
2348
|
+
return path === "" || path === ".." || WHOLE_TREE.has(path);
|
|
2349
|
+
}
|
|
1636
2350
|
function commitsAll(args) {
|
|
1637
2351
|
for (let i = 0;i < args.length; i++) {
|
|
1638
2352
|
const arg = args[i] ?? "";
|
|
@@ -1660,13 +2374,19 @@ function commitsAll(args) {
|
|
|
1660
2374
|
}
|
|
1661
2375
|
function gitViolation(command, isPushAsked = false) {
|
|
1662
2376
|
for (const piece of splitCommand(command)) {
|
|
1663
|
-
const { program, args } = parseCommand(piece.command);
|
|
2377
|
+
const { program, args, assignments } = parseCommand(piece.command);
|
|
2378
|
+
if (assignments.some((assignment) => RISKY_ENV.test(assignment)))
|
|
2379
|
+
return CONFIG_OVERRIDE;
|
|
1664
2380
|
if (program !== "git")
|
|
1665
2381
|
continue;
|
|
2382
|
+
if (overridesConfig(args))
|
|
2383
|
+
return CONFIG_OVERRIDE;
|
|
1666
2384
|
const { name, rest } = subcommandOf(args);
|
|
2385
|
+
if (name === "config" && rest.length > 1 && rest.some((arg) => RISKY_CONFIG.test(arg)) && !rest.some((arg) => READS_CONFIG.has(arg)))
|
|
2386
|
+
return CONFIG_OVERRIDE;
|
|
1667
2387
|
if (name === "stash" && !READ_ONLY_STASH.has(rest[0] ?? ""))
|
|
1668
2388
|
return STASH;
|
|
1669
|
-
if (name === "add" && rest.some(
|
|
2389
|
+
if (name === "add" && rest.some(isWholeTree))
|
|
1670
2390
|
return ADD_ALL;
|
|
1671
2391
|
if (name === "commit" && commitsAll(rest))
|
|
1672
2392
|
return COMMIT_ALL;
|
|
@@ -1767,19 +2487,63 @@ function inlineMessage(parts) {
|
|
|
1767
2487
|
}
|
|
1768
2488
|
|
|
1769
2489
|
// src/remote.ts
|
|
1770
|
-
var SHELL_PROGRAMS = new Set(["ssh", "scp", "sftp", "mosh", "autossh"]);
|
|
2490
|
+
var SHELL_PROGRAMS = new Set(["ssh", "scp", "sftp", "mosh", "mosh-client", "autossh"]);
|
|
2491
|
+
var SOCKET_PROGRAMS = new Set(["nc", "ncat", "netcat", "telnet", "socat"]);
|
|
2492
|
+
var URL_PROGRAMS = new Set(["git", "docker", "docker-compose"]);
|
|
2493
|
+
var DECLARERS2 = new Set(["export", "declare", "typeset"]);
|
|
1771
2494
|
var RSYNC_REMOTE = /^(?:[\w.-]+@)?[\w.-]{2,}:/;
|
|
1772
2495
|
var SSH_URL = /^ssh:\/\//;
|
|
2496
|
+
var SSH_URL_WORD = /^(?:[\w-]+=)?ssh:\/\//;
|
|
2497
|
+
var LOOPBACK = /(?:^|[:=@[])(?:localhost|127(?:\.\d+){3}|::1)(?:[:\]/]|$)/;
|
|
2498
|
+
var LISTENS = /^-[A-KM-Z]*L[A-Z]*$/i;
|
|
2499
|
+
var DOCKER_VALUE_OPTIONS2 = new Set(["-c", "--context", "-H", "--host", "-l", "--log-level", "--config", "--tlscacert", "--tlscert", "--tlskey"]);
|
|
2500
|
+
var CONTEXT_CHANGES = new Set(["use", "create", "update", "import"]);
|
|
2501
|
+
var KUBECTL_SERVER = /^(?:--server(?:=|$)|-s$)/;
|
|
2502
|
+
var REMOTE_VARIABLE = /^GIT_SSH(?:_COMMAND)?=/;
|
|
1773
2503
|
var REMOTE = "Remote servers are off limits (no SSH, no deploys, no remote commands). Hand the user the exact command to run themselves.";
|
|
2504
|
+
function dockerContext(args) {
|
|
2505
|
+
let context = null;
|
|
2506
|
+
for (let i = 0;i < args.length; i++) {
|
|
2507
|
+
const arg = args[i] ?? "";
|
|
2508
|
+
if (arg.startsWith("--context="))
|
|
2509
|
+
context = arg.slice("--context=".length);
|
|
2510
|
+
else if (arg === "-c" || arg === "--context")
|
|
2511
|
+
context = args[++i] ?? null;
|
|
2512
|
+
else if (DOCKER_VALUE_OPTIONS2.has(arg))
|
|
2513
|
+
i++;
|
|
2514
|
+
else if (!arg.startsWith("-"))
|
|
2515
|
+
return { context, name: arg, rest: args.slice(i + 1) };
|
|
2516
|
+
}
|
|
2517
|
+
return { context, name: "", rest: [] };
|
|
2518
|
+
}
|
|
2519
|
+
function isDockerRemote(args, assignments) {
|
|
2520
|
+
const { context, name, rest } = dockerContext(args);
|
|
2521
|
+
const fromEnv = assignments.find((assignment) => assignment.startsWith("DOCKER_CONTEXT="))?.slice("DOCKER_CONTEXT=".length);
|
|
2522
|
+
if ([context, fromEnv].some((each) => each !== null && each !== undefined && each !== "default"))
|
|
2523
|
+
return true;
|
|
2524
|
+
return name === "context" && CONTEXT_CHANGES.has(rest[0] ?? "");
|
|
2525
|
+
}
|
|
2526
|
+
function isSocketRemote(args) {
|
|
2527
|
+
return !args.some((arg) => LISTENS.test(arg) || LOOPBACK.test(arg));
|
|
2528
|
+
}
|
|
1774
2529
|
function isRemote(text) {
|
|
1775
2530
|
const { program, args, assignments } = parseCommand(text);
|
|
2531
|
+
const declared = DECLARERS2.has(program) ? args : [];
|
|
1776
2532
|
if (SHELL_PROGRAMS.has(program))
|
|
1777
2533
|
return true;
|
|
2534
|
+
if (SOCKET_PROGRAMS.has(program))
|
|
2535
|
+
return isSocketRemote(args);
|
|
1778
2536
|
if (program === "rsync")
|
|
1779
2537
|
return args.some((arg) => !arg.startsWith("-") && RSYNC_REMOTE.test(arg));
|
|
1780
|
-
if (
|
|
2538
|
+
if (program === "kubectl")
|
|
2539
|
+
return args.some((arg) => KUBECTL_SERVER.test(arg));
|
|
2540
|
+
if ([...assignments, ...declared].some((assignment) => REMOTE_VARIABLE.test(assignment)))
|
|
2541
|
+
return true;
|
|
2542
|
+
if ([...assignments, ...declared].some((assignment) => assignment.startsWith("DOCKER_HOST=") && SSH_URL.test(assignment.slice("DOCKER_HOST=".length))))
|
|
1781
2543
|
return true;
|
|
1782
|
-
|
|
2544
|
+
if (program === "docker" && isDockerRemote(args, assignments))
|
|
2545
|
+
return true;
|
|
2546
|
+
return URL_PROGRAMS.has(program) && tokenize(text).some((word) => SSH_URL_WORD.test(word));
|
|
1783
2547
|
}
|
|
1784
2548
|
function remoteViolation(command) {
|
|
1785
2549
|
return splitCommand(command).some((piece) => isRemote(piece.command)) ? REMOTE : null;
|
|
@@ -2110,6 +2874,7 @@ function replaceViolation(command) {
|
|
|
2110
2874
|
}
|
|
2111
2875
|
|
|
2112
2876
|
// src/check.ts
|
|
2877
|
+
var UNREADABLE_MESSAGE = "The commit message cannot be read here (a variable, a substitution, a missing file or no heredoc on stdin), so it cannot be checked. Write it literally with `-m` or a `$(cat <<'EOF' ... EOF)` heredoc.";
|
|
2113
2878
|
function absolute(path, cwd) {
|
|
2114
2879
|
return path.startsWith("/") ? path : `${cwd}/${path}`;
|
|
2115
2880
|
}
|
|
@@ -2125,6 +2890,8 @@ function commitViolation(command, { cwd, read }) {
|
|
|
2125
2890
|
message = inlineMessage(source.parts);
|
|
2126
2891
|
else if (file !== "-")
|
|
2127
2892
|
message = read(absolute(file, cwd));
|
|
2893
|
+
if (message === null && (source.parts.length > 0 || file !== null))
|
|
2894
|
+
return UNREADABLE_MESSAGE;
|
|
2128
2895
|
const violation = message === null ? null : commitMessageViolation(message);
|
|
2129
2896
|
if (violation !== null)
|
|
2130
2897
|
return violation;
|
|
@@ -2140,9 +2907,6 @@ function pinnedManagers(start, exists) {
|
|
|
2140
2907
|
return null;
|
|
2141
2908
|
}
|
|
2142
2909
|
}
|
|
2143
|
-
function commandsRun(command) {
|
|
2144
|
-
return [command, ...substitutions(command).flatMap(commandsRun)];
|
|
2145
|
-
}
|
|
2146
2910
|
function mismatches(command, { cwd, home, exists }) {
|
|
2147
2911
|
return installRequests(command, cwd, home).flatMap((request) => {
|
|
2148
2912
|
const mismatch = mismatchFor(request, pinnedManagers(request.dir, exists));
|
|
@@ -2158,13 +2922,22 @@ async function checkCommand(command, context) {
|
|
|
2158
2922
|
};
|
|
2159
2923
|
const run = commandsRun(command);
|
|
2160
2924
|
const first = (messages) => messages.find((message) => message !== null) ?? null;
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2925
|
+
const guarded = async (rule, check) => {
|
|
2926
|
+
try {
|
|
2927
|
+
return await check();
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
context.onError?.(rule, error, command);
|
|
2930
|
+
return `The ${rule} rule crashed on this command (${error instanceof Error ? error.message : String(error)}), so it is refused. Simplify the command or run it yourself.`;
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
const readSql = async (path) => Promise.resolve(context.read(absolute(path, context.cwd)));
|
|
2934
|
+
add("remote", await guarded("remote", () => first(run.map(remoteViolation))));
|
|
2935
|
+
add("git", await guarded("git", () => first(run.map((each) => gitViolation(each, context.isPushAsked)))));
|
|
2936
|
+
const commit = await guarded("commit", () => first(run.map((each) => commitViolation(each, context))));
|
|
2164
2937
|
add("commit", commit === null ? null : `${commit} ${USE_GC}`);
|
|
2165
|
-
add("sql", first(await Promise.all(run.map(async (each) => sqlViolation(each,
|
|
2166
|
-
add("env", touchesEnv(
|
|
2167
|
-
add("edit", context.canSub ? first(run.map(replaceViolation)) : null);
|
|
2938
|
+
add("sql", await guarded("sql", async () => first(await Promise.all(run.map(async (each) => sqlViolation(each, readSql))))));
|
|
2939
|
+
add("env", await guarded("env", () => run.some((each) => touchesEnv(each, { hasTrackedEnv: context.hasTrackedEnv === true })) ? USE_EV : null));
|
|
2940
|
+
add("edit", context.canSub ? await guarded("edit", () => first(run.map(replaceViolation))) : null);
|
|
2168
2941
|
const found = mismatches(command, context);
|
|
2169
2942
|
const refused = found.find((mismatch) => mismatch.rewrite === null || !context.canRewrite);
|
|
2170
2943
|
const rewrite = refused?.rewrite ?? null;
|
|
@@ -2177,10 +2950,10 @@ async function checkCommand(command, context) {
|
|
|
2177
2950
|
}
|
|
2178
2951
|
if (context.canRewrite && context.canWrap && shouldWrap(rewritten))
|
|
2179
2952
|
rewritten = wrap(rewritten);
|
|
2180
|
-
return { violations, command: violations.length > 0 ? command : rewritten, notes: violations.length > 0 ? [] : notes, packages: addedPackages
|
|
2953
|
+
return { violations, command: violations.length > 0 ? command : rewritten, notes: violations.length > 0 ? [] : notes, packages: [...new Set(run.flatMap(addedPackages))] };
|
|
2181
2954
|
}
|
|
2182
2955
|
function checkPath(path) {
|
|
2183
|
-
return
|
|
2956
|
+
return reachesEnvFile(path) ? { rule: "env", message: USE_EV } : null;
|
|
2184
2957
|
}
|
|
2185
2958
|
|
|
2186
2959
|
// src/bins.ts
|
|
@@ -2189,7 +2962,7 @@ var BINS = {
|
|
|
2189
2962
|
fix: "`fix`: runs the repo's fixers and formatters (its fix, lint:fix and format scripts, just fmt, go fmt, cargo fmt, ruff) and lists the files they changed.",
|
|
2190
2963
|
errs: "`errs [log]`, `errs -- <cmd>`: turns tsc, tsgo, eslint, bun check and bun test output into one `file:line:col: message (rule)` line per error, counting the lines it did not recognise (`--json`); use it instead of reading raw type, lint or test logs.",
|
|
2191
2964
|
db: '`db "<sql>"`: one read-only statement against the Postgres in DATABASE_URL (`-e KEY` for another) or, without one, the compose project\'s running Postgres service (`-s <service>`, `-d <database>`), credentials never shown, a write printed for the user to run; `db tables`, `db describe <table>`, `db dbs`.',
|
|
2192
|
-
ev: "`ev`: .env files read and edited with values masked (`ev list`, `ev get KEY`, `ev grep <regex>`, `ev show <rev>:<file>`, `ev set`, `ev diff`, `ev missing`); `ev audit [dir]` finds env files others can read or git tracks, and compose files writing secrets out.",
|
|
2965
|
+
ev: "`ev`: .env files read and edited with values masked (`ev list`, `ev get KEY`, `ev grep <regex>`, `ev show <rev>:<file>`, `ev set`, `ev diff`, `ev missing`); `ev audit [dir]` finds env files others can read or git tracks, and compose files writing secrets out; `ev pins [dir]` lists the env vars reaching outside that the tests of an app leave unpinned.",
|
|
2193
2966
|
redact: "`redact`: masks secrets in piped output, `<cmd> | redact`.",
|
|
2194
2967
|
pkg: "`pkg <name>[@version]`: checks an npm package before it is added: whether the name exists or is a near miss of a popular one, the newest version at least two weeks old, licence, install scripts; `bun add`, `npm install`, `pnpm add` and `yarn add` run it first and are refused on what it flags. `pkg bump <@scope/lib> [version]` waits until the registry serves a release, then `bun add --exact`s it in every consumer under ~/Workspace (`--dry-run`), `pkg consumers <name>` lists them; use it instead of a `bun info` and sleep loop and a `bun add` per repo.",
|
|
2195
2968
|
stack: "`stack port <name>`: the port a service name always hashes to, for every host port a compose file publishes; `stack audit [dir]` cross-checks release.config.ts against its compose files.",
|
|
@@ -2219,9 +2992,21 @@ ${lines.join(`
|
|
|
2219
2992
|
|
|
2220
2993
|
// src/hook.ts
|
|
2221
2994
|
var EVENTS = ["pre-tool-use", "user-prompt-submit", "session-start"];
|
|
2222
|
-
var
|
|
2995
|
+
var PUSH_WORD = /\b(?:push|pousser?|poussez)\b/i;
|
|
2996
|
+
var PUSH_NOUN = /\bpush[\s-]+(?:notifications?|messages?|tokens?|events?|services?|apis?|subscriptions?|workers?|servers?)\b/gi;
|
|
2997
|
+
var NOT_A_REQUEST = /\b(?:don'?t|do not|never|not|no|without|avoid|stop|can'?t|cannot|won'?t|shouldn'?t|did|didn'?t|does|doesn'?t|already|ne|pas|jamais|sans|surtout)\b/i;
|
|
2998
|
+
var CLAUSE_BREAKS = /[.!?;,\n]+/;
|
|
2223
2999
|
var SESSION_ID = /^[\w-]+$/;
|
|
2224
|
-
var FILE_TOOLS = new
|
|
3000
|
+
var FILE_TOOLS = new Map([["Read", "file_path"], ["Edit", "file_path"], ["MultiEdit", "file_path"], ["Write", "file_path"], ["NotebookEdit", "notebook_path"], ["Grep", "path"], ["Glob", "path"]]);
|
|
3001
|
+
var GIT_WORD = /\bgit\b/;
|
|
3002
|
+
var GIT_PRINT_WORD = /\b(?:diff|log|show|whatchanged|format-patch|grep)\b/;
|
|
3003
|
+
var TRACKED_ENV = [":(glob)**/.env", ":(glob)**/.env.*", ":(glob)**/.envrc"];
|
|
3004
|
+
function asksForPush(prompt) {
|
|
3005
|
+
return prompt.split(CLAUSE_BREAKS).some((clause) => {
|
|
3006
|
+
const asked = clause.replace(PUSH_NOUN, "");
|
|
3007
|
+
return PUSH_WORD.test(asked) && !NOT_A_REQUEST.test(asked);
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
2225
3010
|
function pushFile(sessionId, { stateDir }) {
|
|
2226
3011
|
return sessionId !== undefined && SESSION_ID.test(sessionId) ? `${stateDir}/push-asked/${sessionId}` : null;
|
|
2227
3012
|
}
|
|
@@ -2243,25 +3028,36 @@ async function bashCall(command, input, machine) {
|
|
|
2243
3028
|
canWrap: false,
|
|
2244
3029
|
canSub: machine.has("sub"),
|
|
2245
3030
|
read: machine.read,
|
|
2246
|
-
exists: machine.exists
|
|
3031
|
+
exists: machine.exists,
|
|
3032
|
+
hasTrackedEnv: GIT_WORD.test(command) && GIT_PRINT_WORD.test(command) && await hasTrackedEnv(cwd, machine),
|
|
3033
|
+
onError: (rule, error, each) => machine.log?.(`${new Date().toISOString()} ${rule} crashed: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
3034
|
+
command: ${each}
|
|
3035
|
+
`)
|
|
2247
3036
|
});
|
|
2248
3037
|
if (verdict.violations.length > 0 || verdict.packages.length === 0 || !machine.has("pkg"))
|
|
2249
3038
|
return deny(verdict.violations);
|
|
2250
3039
|
const checked = await machine.run(["pkg", "check", ...verdict.packages], cwd);
|
|
2251
|
-
|
|
2252
|
-
|
|
3040
|
+
if (checked.code === 0)
|
|
3041
|
+
return null;
|
|
3042
|
+
const message = checked.code === 1 ? "pkg refused what this adds, so it was not run" : `pkg check failed (exit ${checked.code}), so what this adds was not checked and it was not run`;
|
|
3043
|
+
return deny([{ rule: "pkg", message: `${message}:
|
|
3044
|
+
${checked.text.trim()}` }]);
|
|
3045
|
+
}
|
|
3046
|
+
async function hasTrackedEnv(cwd, machine) {
|
|
3047
|
+
const listed = await machine.run(["git", "ls-files", "-z", "--", ...TRACKED_ENV], cwd);
|
|
3048
|
+
return listed.code === 0 && listed.text.split("\x00").some(isEnvFile);
|
|
2253
3049
|
}
|
|
2254
3050
|
function preToolUse(input, machine) {
|
|
2255
3051
|
const { tool_name: tool, tool_input: given = {} } = input;
|
|
2256
3052
|
if (tool === "Bash" && typeof given.command === "string")
|
|
2257
3053
|
return bashCall(given.command, input, machine);
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
return null;
|
|
3054
|
+
const field = tool === undefined ? undefined : FILE_TOOLS.get(tool);
|
|
3055
|
+
const paths = [field === undefined ? undefined : given[field], tool === "Grep" ? given.glob : undefined];
|
|
3056
|
+
return deny(paths.flatMap((path) => typeof path === "string" ? [checkPath(path)] : []).filter((each) => each !== null));
|
|
2261
3057
|
}
|
|
2262
3058
|
function userPromptSubmit(input, machine) {
|
|
2263
3059
|
const file = pushFile(input.session_id, machine);
|
|
2264
|
-
if (file !== null &&
|
|
3060
|
+
if (file !== null && asksForPush(input.prompt ?? ""))
|
|
2265
3061
|
machine.write(file, "");
|
|
2266
3062
|
else if (file !== null)
|
|
2267
3063
|
machine.remove(file);
|
|
@@ -2302,13 +3098,15 @@ var pathOptions = {
|
|
|
2302
3098
|
var hookUsage = `guard hook <${EVENTS.join("|")}>
|
|
2303
3099
|
|
|
2304
3100
|
Answers a Claude Code hook: reads the event JSON on stdin and prints the reply JSON, nothing when the call goes ahead untouched.
|
|
2305
|
-
pre-tool-use refuses a Bash call breaking a rule, an install pkg refuses, and a Read, Edit
|
|
3101
|
+
pre-tool-use refuses a Bash call breaking a rule, an install pkg refuses (or fails to check), and a file tool (Read, Edit, MultiEdit, Write, NotebookEdit, Grep, Glob) reaching an env file; user-prompt-submit remembers whether the prompt asked for a push; session-start lists the installed agent-tools bins.
|
|
2306
3102
|
A failure exits 1, which Claude Code reports without blocking the call.
|
|
2307
3103
|
|
|
2308
3104
|
Exits 0 when it answered, 1 on a failure or an unknown event (never 2, which would block the call).`;
|
|
3105
|
+
var MAX_READ = 1e6;
|
|
2309
3106
|
function readText(path) {
|
|
2310
3107
|
try {
|
|
2311
|
-
|
|
3108
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
3109
|
+
return stats?.isFile() === true && stats.size <= MAX_READ ? readFileSync(path, "utf8") : null;
|
|
2312
3110
|
} catch {
|
|
2313
3111
|
return null;
|
|
2314
3112
|
}
|
|
@@ -2327,7 +3125,7 @@ ${checkUsage}`);
|
|
|
2327
3125
|
const given = positionals[0] ?? "-";
|
|
2328
3126
|
const command = given === "-" ? await readInput("-", { noStdin: "give the command, or pipe it on stdin" }) : given;
|
|
2329
3127
|
const context = {
|
|
2330
|
-
cwd: values.cwd ??
|
|
3128
|
+
cwd: values.cwd ?? process7.cwd(),
|
|
2331
3129
|
home: homedir(),
|
|
2332
3130
|
isPushAsked: values["push-asked"] === true,
|
|
2333
3131
|
canRewrite: true,
|
|
@@ -2361,9 +3159,10 @@ ${pathUsage}`);
|
|
|
2361
3159
|
}
|
|
2362
3160
|
function machine() {
|
|
2363
3161
|
const home = homedir();
|
|
3162
|
+
const stateDir = `${process7.env.XDG_STATE_HOME ?? `${home}/.local/state`}/guard`;
|
|
2364
3163
|
return {
|
|
2365
3164
|
home,
|
|
2366
|
-
stateDir
|
|
3165
|
+
stateDir,
|
|
2367
3166
|
has: (bin) => Bun.which(bin) !== null,
|
|
2368
3167
|
run: async (argv, cwd) => runCommand(argv, { cwd }),
|
|
2369
3168
|
read: readText,
|
|
@@ -2372,7 +3171,13 @@ function machine() {
|
|
|
2372
3171
|
mkdirSync(dirname(path), { recursive: true });
|
|
2373
3172
|
writeFileSync(path, text);
|
|
2374
3173
|
},
|
|
2375
|
-
remove: (path) => rmSync(path, { force: true })
|
|
3174
|
+
remove: (path) => rmSync(path, { force: true }),
|
|
3175
|
+
log: (text) => {
|
|
3176
|
+
try {
|
|
3177
|
+
mkdirSync(stateDir, { recursive: true });
|
|
3178
|
+
appendFileSync(`${stateDir}/errors.log`, text);
|
|
3179
|
+
} catch {}
|
|
3180
|
+
}
|
|
2376
3181
|
};
|
|
2377
3182
|
}
|
|
2378
3183
|
async function hookCommand(args) {
|
|
@@ -2387,7 +3192,7 @@ ${hookUsage}`, 1);
|
|
|
2387
3192
|
console.log(JSON.stringify(reply));
|
|
2388
3193
|
return 0;
|
|
2389
3194
|
}
|
|
2390
|
-
|
|
3195
|
+
process7.exitCode = await runCli({
|
|
2391
3196
|
name: "guard",
|
|
2392
3197
|
version,
|
|
2393
3198
|
about: "the safety rules a Claude Code Bash call is held to, each refusal naming the route",
|