@kaynooo/guard-tools 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1034 -185
- 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";
|
|
@@ -298,6 +298,26 @@ function upTo(text, separator) {
|
|
|
298
298
|
}
|
|
299
299
|
var ESCAPE = String.fromCharCode(27);
|
|
300
300
|
|
|
301
|
+
// ../kit/src/text.ts
|
|
302
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, "gi");
|
|
303
|
+
function distance(a, b) {
|
|
304
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
305
|
+
for (const [i, left] of [...a].entries()) {
|
|
306
|
+
const current = [i + 1];
|
|
307
|
+
for (const [j, right] of [...b].entries())
|
|
308
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)));
|
|
309
|
+
previous = current;
|
|
310
|
+
}
|
|
311
|
+
return previous[b.length] ?? 0;
|
|
312
|
+
}
|
|
313
|
+
function closest(name, names) {
|
|
314
|
+
const started = names.find((known) => known.startsWith(name));
|
|
315
|
+
if (started !== undefined)
|
|
316
|
+
return started;
|
|
317
|
+
const [best] = names.map((known) => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far);
|
|
318
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
301
321
|
// ../kit/src/args.ts
|
|
302
322
|
function jsonOption(holds) {
|
|
303
323
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
@@ -317,8 +337,8 @@ function fail(message, code = 2, hint) {
|
|
|
317
337
|
}
|
|
318
338
|
|
|
319
339
|
class UsageError extends Exit {
|
|
320
|
-
constructor(message) {
|
|
321
|
-
super(message, 2);
|
|
340
|
+
constructor(message, hint) {
|
|
341
|
+
super(message, 2, hint);
|
|
322
342
|
}
|
|
323
343
|
}
|
|
324
344
|
function badUsage(message) {
|
|
@@ -341,6 +361,7 @@ var completion = { active: false };
|
|
|
341
361
|
var OPTIONAL = /\[[^\]]+\]/g;
|
|
342
362
|
var USAGE_PREFIX = /^usage:\s*/i;
|
|
343
363
|
var FULL_STOP = /\.$/;
|
|
364
|
+
var DASHES = /^-+/;
|
|
344
365
|
function usageLine(usage, colors = paint(process3.stdout)) {
|
|
345
366
|
return `${colors.bold("Usage:")} ${usage.replace(USAGE_PREFIX, "").replaceAll(OPTIONAL, (part) => colors.dim(part))}`;
|
|
346
367
|
}
|
|
@@ -377,10 +398,10 @@ function optionRows(options, colors = paint(process3.stdout)) {
|
|
|
377
398
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
378
399
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
379
400
|
const value = option.type === "string" ? ` ${colors.dim("<value>")}` : "";
|
|
380
|
-
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description };
|
|
401
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? " <value>".length : 0), description: option.description ?? "" };
|
|
381
402
|
});
|
|
382
403
|
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
383
|
-
return rows.map((row) => ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
404
|
+
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
384
405
|
}
|
|
385
406
|
var OUTPUT_OPTIONS = {
|
|
386
407
|
color: { type: "string", description: "auto (on a terminal alone), always or never" },
|
|
@@ -412,7 +433,8 @@ function parse(args, usage, options, settings = {}) {
|
|
|
412
433
|
try {
|
|
413
434
|
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
414
435
|
} catch (error) {
|
|
415
|
-
|
|
436
|
+
const message = parseError(error);
|
|
437
|
+
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
416
438
|
}
|
|
417
439
|
const values = parsed.values;
|
|
418
440
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
@@ -455,6 +477,22 @@ function parseError(error) {
|
|
|
455
477
|
const message = upTo(error instanceof Error ? error.message : String(error), ". ");
|
|
456
478
|
return `${message.charAt(0).toLowerCase()}${message.slice(1)}`;
|
|
457
479
|
}
|
|
480
|
+
var UNKNOWN_OPTION = /^unknown option '(-[^']+)'/;
|
|
481
|
+
function nearestOption(message, options, allowNegative) {
|
|
482
|
+
const typed = UNKNOWN_OPTION.exec(message)?.[1];
|
|
483
|
+
if (typed === undefined)
|
|
484
|
+
return;
|
|
485
|
+
const word = typed.replace(DASHES, "");
|
|
486
|
+
if (word.length < 2 && !typed.startsWith("--"))
|
|
487
|
+
return;
|
|
488
|
+
const known = Object.entries(options).flatMap(([name, option]) => [
|
|
489
|
+
...option.short === undefined ? [] : [`-${option.short}`],
|
|
490
|
+
`--${name}`,
|
|
491
|
+
...allowNegative && option.type === "boolean" ? [`--no-${name}`] : []
|
|
492
|
+
]);
|
|
493
|
+
const meant = closest(word, known.map((flag) => flag.replace(DASHES, "")));
|
|
494
|
+
return meant === undefined ? undefined : `did you mean ${known.find((flag) => flag.replace(DASHES, "") === meant)}?`;
|
|
495
|
+
}
|
|
458
496
|
async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
459
497
|
if (path === "-") {
|
|
460
498
|
if (process3.stdin.isTTY)
|
|
@@ -466,35 +504,116 @@ async function readInput(path, { noStdin = "nothing is piped on stdin" } = {}) {
|
|
|
466
504
|
fail(`no file ${path}`);
|
|
467
505
|
return file.text();
|
|
468
506
|
}
|
|
469
|
-
// ../kit/src/
|
|
507
|
+
// ../kit/src/process.ts
|
|
508
|
+
import { constants } from "os";
|
|
470
509
|
import process4 from "process";
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
+
};
|
|
483
595
|
}
|
|
484
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}\\`;
|
|
485
602
|
// ../kit/src/cli.ts
|
|
603
|
+
import process5 from "process";
|
|
486
604
|
var LEVEL_OPTIONS = {
|
|
487
605
|
...OUTPUT_OPTIONS,
|
|
488
606
|
help: { type: "boolean", short: "h", description: "show this help" }
|
|
489
607
|
};
|
|
490
608
|
var TOP_OPTIONS = { ...LEVEL_OPTIONS, version: { type: "boolean", short: "v", description: "print the version" } };
|
|
491
|
-
function commandRows(commands, colors) {
|
|
609
|
+
async function commandRows(commands, colors) {
|
|
492
610
|
const width = Math.max(...Object.keys(commands).map((name) => name.length));
|
|
493
611
|
const groups = new Map([["Commands:", []]]);
|
|
494
|
-
|
|
612
|
+
const summaries = await Promise.all(Object.values(commands).map(async ({ summary }) => resolveSummary(summary)));
|
|
613
|
+
for (const [index, [name, { group }]] of Object.entries(commands).entries()) {
|
|
495
614
|
const heading = group === undefined ? "Commands:" : `${group}:`;
|
|
496
615
|
const rows = groups.get(heading) ?? [];
|
|
497
|
-
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(
|
|
616
|
+
rows.push(` ${colors.cyan(name.padEnd(width))} ${colors.dim(summaries[index])}`);
|
|
498
617
|
groups.set(heading, rows);
|
|
499
618
|
}
|
|
500
619
|
return [...groups].filter(([, rows]) => rows.length > 0).flatMap(([heading, rows], index) => [...index === 0 ? [] : [""], colors.bold(heading), ...rows]);
|
|
@@ -514,28 +633,38 @@ function takeOutputFlags(argv) {
|
|
|
514
633
|
}
|
|
515
634
|
return argv.slice(index);
|
|
516
635
|
}
|
|
517
|
-
function
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined;
|
|
636
|
+
async function resolveSummary(summary) {
|
|
637
|
+
return typeof summary === "string" ? summary : summary();
|
|
638
|
+
}
|
|
639
|
+
function scopeOf(commands, extra, aliases) {
|
|
640
|
+
return { commands: typeof commands === "function" ? {} : commands, lazy: [...typeof commands === "function" ? [commands] : [], ...extra === undefined ? [] : [extra]], aliases };
|
|
523
641
|
}
|
|
524
|
-
async function
|
|
642
|
+
async function expand(scope, words) {
|
|
643
|
+
if (scope.lazy.length === 0)
|
|
644
|
+
return scope;
|
|
645
|
+
const commands = { ...scope.commands };
|
|
646
|
+
for (const added of await Promise.all(scope.lazy.map(async (lazy) => lazy(words)))) {
|
|
647
|
+
for (const [name, command] of Object.entries(added))
|
|
648
|
+
commands[name] ??= command;
|
|
649
|
+
}
|
|
650
|
+
return { ...scope, commands, lazy: [] };
|
|
651
|
+
}
|
|
652
|
+
async function help({ path, header, summary: lazySummary, commands, defaultCommand, examples, replaces }, stream) {
|
|
525
653
|
const colors = paint(stream);
|
|
526
654
|
const prefix = path.join(" ");
|
|
655
|
+
const summary = await resolveSummary(lazySummary);
|
|
527
656
|
if (wantsHelpJson())
|
|
528
657
|
return JSON.stringify(description(`${prefix} <command> [options]: ${summary}`, { examples, replaces }));
|
|
529
658
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
530
659
|
const options = fallback !== undefined && "run" in fallback ? await optionsOf(fallback.run) : undefined;
|
|
531
660
|
const own = Object.entries(options ?? {}).filter(([name]) => !(name in TOP_OPTIONS));
|
|
532
661
|
return [
|
|
533
|
-
header(colors),
|
|
662
|
+
header(colors, summary),
|
|
534
663
|
"",
|
|
535
664
|
usageLine(`${prefix} ${defaultCommand === undefined ? colors.cyan("<command>") : `[${colors.cyan("<command>")}]`} [options]`, colors),
|
|
536
665
|
...replacesRows(replaces, colors),
|
|
537
666
|
"",
|
|
538
|
-
...commandRows(commands, colors),
|
|
667
|
+
...await commandRows(commands, colors),
|
|
539
668
|
...own.length === 0 ? [] : ["", `${colors.bold(`Options of ${defaultCommand}:`)} ${colors.dim("the command run when none is named")}`, ...optionRows(Object.fromEntries(own), colors)],
|
|
540
669
|
"",
|
|
541
670
|
colors.bold("Options:"),
|
|
@@ -547,8 +676,8 @@ async function help({ path, header, summary, commands, defaultCommand, examples,
|
|
|
547
676
|
`);
|
|
548
677
|
}
|
|
549
678
|
function printError(prefix, message, hint, cause) {
|
|
550
|
-
const colors = paint(
|
|
551
|
-
const styled = takesControlCodes(
|
|
679
|
+
const colors = paint(process5.stderr);
|
|
680
|
+
const styled = takesControlCodes(process5.stderr);
|
|
552
681
|
const head = styled ? `${colors.red.bold("error")} ${colors.dim(`${prefix}:`)}` : `error: ${prefix}:`;
|
|
553
682
|
const under = (label, text, tint) => text === undefined ? "" : `
|
|
554
683
|
${styled ? tint(label) : `${label}:`} ${text}`;
|
|
@@ -581,11 +710,11 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
581
710
|
}
|
|
582
711
|
const [name, ...args] = argv;
|
|
583
712
|
if (name === undefined && level.path.length > 1) {
|
|
584
|
-
console.error(await help(level,
|
|
713
|
+
console.error(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stderr));
|
|
585
714
|
return 2;
|
|
586
715
|
}
|
|
587
716
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
588
|
-
console.log(await help(level,
|
|
717
|
+
console.log(await help(await expand(level, [...level.path.slice(1), ...argv]), process5.stdout));
|
|
589
718
|
return 0;
|
|
590
719
|
}
|
|
591
720
|
if (level.path.length === 1 && (name === "--version" || name === "-v")) {
|
|
@@ -594,6 +723,10 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
594
723
|
}
|
|
595
724
|
let command = level.commands[level.aliases?.[name] ?? name];
|
|
596
725
|
let rest = args;
|
|
726
|
+
if (command === undefined && level.lazy.length > 0) {
|
|
727
|
+
level = await expand(level, [...level.path.slice(1), ...argv]);
|
|
728
|
+
command = level.commands[level.aliases?.[name] ?? name];
|
|
729
|
+
}
|
|
597
730
|
if (command === undefined && level.defaultCommand !== undefined) {
|
|
598
731
|
command = level.commands[level.defaultCommand];
|
|
599
732
|
rest = argv;
|
|
@@ -605,7 +738,7 @@ async function dispatch(level, typed, { version, redact }) {
|
|
|
605
738
|
}
|
|
606
739
|
if ("commands" in command) {
|
|
607
740
|
const { summary } = command;
|
|
608
|
-
return dispatch({ path: [...level.path, name], header: (colors) => `${colors.bold(`${prefix} ${name}`)} ${
|
|
741
|
+
return dispatch({ path: [...level.path, name], header: (colors, text) => `${colors.bold(`${prefix} ${name}`)} ${text}`, summary, ...scopeOf(command.commands, command.extraCommands, command.aliases), defaultCommand: undefined, examples: command.examples, replaces: undefined }, rest, { version, redact });
|
|
609
742
|
}
|
|
610
743
|
try {
|
|
611
744
|
return await command.run(rest);
|
|
@@ -630,67 +763,104 @@ async function optionsOf(run) {
|
|
|
630
763
|
async function leafCandidates(run, words) {
|
|
631
764
|
if (!(words.at(-1) ?? "").startsWith("-"))
|
|
632
765
|
return [FILES];
|
|
633
|
-
return Object.entries(await optionsOf(run)).map(([name, option]) => `--${name} ${option.description}`);
|
|
766
|
+
return Object.entries(await optionsOf(run)).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
634
767
|
}
|
|
635
|
-
async function candidates(
|
|
768
|
+
async function candidates(scope, defaultCommand, words, before = []) {
|
|
636
769
|
const [word = "", ...rest] = words;
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
770
|
+
const listing = rest.length === 0 && !word.startsWith("-");
|
|
771
|
+
let { commands, aliases } = scope;
|
|
772
|
+
let command = commands[aliases?.[word] ?? word];
|
|
773
|
+
if (listing || command === undefined) {
|
|
774
|
+
({ commands, aliases } = await expand(scope, [...before, ...words]));
|
|
775
|
+
command = commands[aliases?.[word] ?? word];
|
|
776
|
+
}
|
|
777
|
+
if (listing)
|
|
778
|
+
return Promise.all(Object.entries(commands).map(async ([name, { summary }]) => `${name} ${await resolveSummary(summary)}`));
|
|
640
779
|
if (command === undefined) {
|
|
641
780
|
const fallback = defaultCommand === undefined ? undefined : commands[defaultCommand];
|
|
642
781
|
return fallback !== undefined && "run" in fallback ? leafCandidates(fallback.run, words) : [];
|
|
643
782
|
}
|
|
644
|
-
return "commands" in command ? candidates(command.commands, command.aliases, undefined, rest) : leafCandidates(command.run, rest);
|
|
783
|
+
return "commands" in command ? candidates(scopeOf(command.commands, command.extraCommands, command.aliases), undefined, rest, [...before, word]) : leafCandidates(command.run, rest);
|
|
645
784
|
}
|
|
646
785
|
async function printCandidates(found) {
|
|
647
786
|
for (const line of await found)
|
|
648
787
|
console.log(line);
|
|
649
788
|
return 0;
|
|
650
789
|
}
|
|
651
|
-
async function runCli(cli, argv =
|
|
790
|
+
async function runCli(cli, argv = process5.argv.slice(2)) {
|
|
652
791
|
if (argv[0] === COMPLETE)
|
|
653
|
-
return printCandidates(candidates(cli.commands, cli.aliases, cli.defaultCommand, argv.slice(1)));
|
|
654
|
-
return dispatch({ path: [cli.name], header: (colors) => `${colors.bold(cli.name)} ${colors.dim(cli.version)} ${
|
|
655
|
-
}
|
|
656
|
-
// ../kit/src/process.ts
|
|
657
|
-
import { constants } from "os";
|
|
658
|
-
async function collect(stream, name, chunks) {
|
|
659
|
-
for await (const data of stream)
|
|
660
|
-
chunks.push({ stream: name, data });
|
|
661
|
-
}
|
|
662
|
-
function exitCodeOf(code, signal) {
|
|
663
|
-
if (code !== null)
|
|
664
|
-
return code;
|
|
665
|
-
if (typeof signal === "number")
|
|
666
|
-
return 128 + signal;
|
|
667
|
-
return signal === null ? 129 : 128 + constants.signals[signal];
|
|
668
|
-
}
|
|
669
|
-
async function runCommand(argv, { cwd, env, stdin = "ignore" } = {}) {
|
|
670
|
-
const chunks = [];
|
|
671
|
-
const proc = Bun.spawn(argv, { ...cwd === undefined ? {} : { cwd }, ...env === undefined ? {} : { env }, stdin, stdout: "pipe", stderr: "pipe" });
|
|
672
|
-
await Promise.all([collect(proc.stdout, "stdout", chunks), collect(proc.stderr, "stderr", chunks)]);
|
|
673
|
-
await proc.exited;
|
|
674
|
-
const output = Buffer.concat(chunks.map(({ data }) => data));
|
|
675
|
-
return { code: exitCodeOf(proc.exitCode, proc.signalCode), chunks, output, text: output.toString() };
|
|
792
|
+
return printCandidates(candidates(scopeOf(cli.commands, cli.extraCommands, cli.aliases), cli.defaultCommand, argv.slice(1)));
|
|
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 });
|
|
676
794
|
}
|
|
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"]);
|
|
677
799
|
// package.json
|
|
678
|
-
var version = "0.
|
|
800
|
+
var version = "0.5.0";
|
|
679
801
|
|
|
680
802
|
// ../kit/src/shell.ts
|
|
681
803
|
var SEPARATORS = new Set([`
|
|
682
804
|
`, ";", "|"]);
|
|
683
805
|
var PLAIN_BREAKERS = new Set(["<", ">", "(", ")", "&", "|"]);
|
|
806
|
+
var GLOB_CHARS = new Set(["*", "?", "["]);
|
|
807
|
+
var COMMENT_PREFIXES = new Set([";", "&", "|", "(", ")"]);
|
|
684
808
|
var WHITESPACE = /\s/;
|
|
685
809
|
var ASSIGNMENT = /^\w+=/;
|
|
686
810
|
var TRAILING_SLASHES = /\/+$/;
|
|
811
|
+
var TRAILING_CR = /\r$/;
|
|
687
812
|
var LEADING_TILDE = /^~(?=\/|$)/;
|
|
688
813
|
var LEADING_TABS = /^\t+/;
|
|
689
814
|
var OPENERS = /^[({]+/;
|
|
690
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;
|
|
691
821
|
var DOUBLE_QUOTED_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
692
822
|
var HEREDOC = /^<<(-?)[ \t]*(?:'([^']*)'|"([^"]*)"|([^\s;&|<>()-][^\s;&|<>()]*))/;
|
|
693
|
-
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
|
+
}
|
|
694
864
|
function separatorWidth(command, i) {
|
|
695
865
|
const pair = command.slice(i, i + 2);
|
|
696
866
|
if (pair === "&&" || pair === "||")
|
|
@@ -707,7 +877,7 @@ function heredocEnd(command, newline, words) {
|
|
|
707
877
|
const next = command.indexOf(`
|
|
708
878
|
`, lineStart);
|
|
709
879
|
const lineEnd = next === -1 ? command.length : next;
|
|
710
|
-
const line = command.slice(lineStart, lineEnd);
|
|
880
|
+
const line = command.slice(lineStart, lineEnd).replace(TRAILING_CR, "");
|
|
711
881
|
lineStart = lineEnd + 1;
|
|
712
882
|
if ((stripsTabs ? line.replace(LEADING_TABS, "") : line) === word)
|
|
713
883
|
break;
|
|
@@ -729,6 +899,7 @@ function withoutRanges(command, start, end, ranges) {
|
|
|
729
899
|
function splitCommand(command) {
|
|
730
900
|
const bounds = [];
|
|
731
901
|
const bodies = [];
|
|
902
|
+
const comments = [];
|
|
732
903
|
let heredocs = [];
|
|
733
904
|
let start = 0;
|
|
734
905
|
let quote = null;
|
|
@@ -739,14 +910,27 @@ function splitCommand(command) {
|
|
|
739
910
|
continue;
|
|
740
911
|
}
|
|
741
912
|
if (quote !== null) {
|
|
742
|
-
if (char === quote)
|
|
913
|
+
if (char === closerOf(quote))
|
|
743
914
|
quote = null;
|
|
744
915
|
continue;
|
|
745
916
|
}
|
|
917
|
+
if (char === "$" && command[i + 1] === "'") {
|
|
918
|
+
quote = ANSI_QUOTE;
|
|
919
|
+
i++;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
746
922
|
if (char === "'" || char === '"') {
|
|
747
923
|
quote = char;
|
|
748
924
|
continue;
|
|
749
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
|
+
}
|
|
750
934
|
const heredoc = char === "<" && command[i + 1] === "<" && command[i + 2] !== "<" ? HEREDOC.exec(command.slice(i)) : null;
|
|
751
935
|
if (heredoc) {
|
|
752
936
|
heredocs.push({ word: heredoc[2] ?? heredoc[3] ?? heredoc[4] ?? "", stripsTabs: heredoc[1] === "-", isQuoted: heredoc[4] === undefined });
|
|
@@ -769,32 +953,78 @@ function splitCommand(command) {
|
|
|
769
953
|
start = i + 1;
|
|
770
954
|
}
|
|
771
955
|
bounds.push([start, command.length, ""]);
|
|
772
|
-
const ranges = bodies.map(([from, to]) => [from, to]);
|
|
773
|
-
return bounds.map(([from, to, separator]) =>
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
});
|
|
781
969
|
}
|
|
782
970
|
function isPlain(text) {
|
|
783
971
|
let quote = null;
|
|
784
972
|
for (let i = 0;i < text.length; i++) {
|
|
785
973
|
const char = text[i] ?? "";
|
|
786
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
|
+
}
|
|
787
980
|
if (char === "\\" && quote !== "'")
|
|
788
981
|
i++;
|
|
789
|
-
else if (quote !== null && char === quote)
|
|
982
|
+
else if (quote !== null && char === closerOf(quote))
|
|
790
983
|
quote = null;
|
|
791
984
|
else if (quote === null && (char === '"' || char === "'"))
|
|
792
985
|
quote = char;
|
|
793
|
-
else if (quote !== "'" && (isSubstitution || quote === null && PLAIN_BREAKERS.has(char)))
|
|
986
|
+
else if (quote !== "'" && quote !== ANSI_QUOTE && (isSubstitution || quote === null && PLAIN_BREAKERS.has(char)))
|
|
794
987
|
return false;
|
|
795
988
|
}
|
|
796
989
|
return quote === null;
|
|
797
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
|
+
}
|
|
798
1028
|
function closingParen(text, open) {
|
|
799
1029
|
let depth = 0;
|
|
800
1030
|
let quote = null;
|
|
@@ -803,8 +1033,11 @@ function closingParen(text, open) {
|
|
|
803
1033
|
if (char === "\\" && quote !== "'") {
|
|
804
1034
|
i++;
|
|
805
1035
|
} else if (quote !== null) {
|
|
806
|
-
if (char === quote)
|
|
1036
|
+
if (char === closerOf(quote))
|
|
807
1037
|
quote = null;
|
|
1038
|
+
} else if (char === "$" && text[i + 1] === "'") {
|
|
1039
|
+
quote = ANSI_QUOTE;
|
|
1040
|
+
i++;
|
|
808
1041
|
} else if (char === "'" || char === '"') {
|
|
809
1042
|
quote = char;
|
|
810
1043
|
} else if (char === "(") {
|
|
@@ -829,12 +1062,18 @@ function bodies(text, isQuoting) {
|
|
|
829
1062
|
let quote = null;
|
|
830
1063
|
for (let i = 0;i < text.length; i++) {
|
|
831
1064
|
const char = text[i] ?? "";
|
|
1065
|
+
const next = text[i + 1];
|
|
832
1066
|
if (quote === "'") {
|
|
833
1067
|
if (char === "'")
|
|
834
1068
|
quote = null;
|
|
1069
|
+
} else if (quote === ANSI_QUOTE) {
|
|
1070
|
+
if (char === "\\")
|
|
1071
|
+
i++;
|
|
1072
|
+
else if (char === "'")
|
|
1073
|
+
quote = null;
|
|
835
1074
|
} else if (char === "\\") {
|
|
836
1075
|
i++;
|
|
837
|
-
} else if (char === "$" &&
|
|
1076
|
+
} else if (char === "$" && next === "(" || isQuoting && quote === null && (char === "<" || char === ">") && next === "(") {
|
|
838
1077
|
const end = closingParen(text, i + 1);
|
|
839
1078
|
found.push(text.slice(i + 2, end));
|
|
840
1079
|
i = end;
|
|
@@ -844,6 +1083,9 @@ function bodies(text, isQuoting) {
|
|
|
844
1083
|
i = end;
|
|
845
1084
|
} else if (isQuoting && char === '"') {
|
|
846
1085
|
quote = quote === null ? char : null;
|
|
1086
|
+
} else if (isQuoting && char === "$" && next === "'" && quote === null) {
|
|
1087
|
+
quote = ANSI_QUOTE;
|
|
1088
|
+
i++;
|
|
847
1089
|
} else if (isQuoting && char === "'" && quote === null) {
|
|
848
1090
|
quote = char;
|
|
849
1091
|
}
|
|
@@ -853,11 +1095,26 @@ function bodies(text, isQuoting) {
|
|
|
853
1095
|
function substitutions(command) {
|
|
854
1096
|
return splitCommand(command).flatMap((piece) => [...bodies(piece.command, true), ...bodies(piece.expanded, false)]);
|
|
855
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
|
+
}
|
|
856
1111
|
function words(text) {
|
|
857
1112
|
const found = [];
|
|
858
1113
|
let current = "";
|
|
859
1114
|
let isStarted = false;
|
|
860
1115
|
let isProse = false;
|
|
1116
|
+
let hasGlob = false;
|
|
1117
|
+
let hasBrace = false;
|
|
861
1118
|
let quote = null;
|
|
862
1119
|
for (let i = 0;i < text.length; i++) {
|
|
863
1120
|
const char = text[i] ?? "";
|
|
@@ -867,40 +1124,79 @@ function words(text) {
|
|
|
867
1124
|
i++;
|
|
868
1125
|
if (quote === null && (isContinuation || WHITESPACE.test(char))) {
|
|
869
1126
|
if (isStarted)
|
|
870
|
-
found.push({ text: current, isProse });
|
|
1127
|
+
found.push({ text: current, isProse, hasGlob, hasBrace });
|
|
871
1128
|
current = "";
|
|
872
1129
|
isStarted = false;
|
|
873
1130
|
isProse = false;
|
|
1131
|
+
hasGlob = false;
|
|
1132
|
+
hasBrace = false;
|
|
874
1133
|
continue;
|
|
875
1134
|
}
|
|
876
1135
|
if (isContinuation)
|
|
877
1136
|
continue;
|
|
878
1137
|
isStarted = true;
|
|
879
|
-
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] ?? "")) {
|
|
880
1143
|
current += char;
|
|
881
|
-
else if (char === "\\" && quote !== "'")
|
|
1144
|
+
} else if (char === "\\" && quote !== "'") {
|
|
882
1145
|
current += text[++i] ?? "";
|
|
883
|
-
else if (quote !== null && char === quote)
|
|
1146
|
+
} else if (quote !== null && char === closerOf(quote)) {
|
|
884
1147
|
quote = null;
|
|
885
|
-
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 === "'")) {
|
|
886
1152
|
quote = char;
|
|
887
|
-
else
|
|
1153
|
+
} else {
|
|
888
1154
|
current += char;
|
|
1155
|
+
hasGlob ||= quote === null && GLOB_CHARS.has(char);
|
|
1156
|
+
hasBrace ||= quote === null && char === "{";
|
|
1157
|
+
}
|
|
889
1158
|
isProse ||= quote !== null && WHITESPACE.test(char);
|
|
890
1159
|
}
|
|
891
1160
|
if (isStarted)
|
|
892
|
-
found.push({ text: current, isProse });
|
|
1161
|
+
found.push({ text: current, isProse, hasGlob, hasBrace });
|
|
893
1162
|
return found;
|
|
894
1163
|
}
|
|
895
1164
|
function tokenize(text) {
|
|
896
1165
|
return words(text).map((word) => word.text);
|
|
897
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
|
+
}
|
|
898
1193
|
function parseCommand(text) {
|
|
899
1194
|
const tokens = tokenize(text);
|
|
900
1195
|
const assignments = [];
|
|
901
1196
|
let isWrapped = false;
|
|
902
1197
|
for (;; ) {
|
|
903
1198
|
const first = tokens[0] ?? "";
|
|
1199
|
+
const spec = WRAPPERS.get(first.split("/").pop() ?? "");
|
|
904
1200
|
if (OPENERS.test(first)) {
|
|
905
1201
|
isWrapped = true;
|
|
906
1202
|
const rest = first.replace(OPENERS, "");
|
|
@@ -910,11 +1206,20 @@ function parseCommand(text) {
|
|
|
910
1206
|
tokens[0] = rest;
|
|
911
1207
|
} else if (ASSIGNMENT.test(first)) {
|
|
912
1208
|
assignments.push(tokens.shift() ?? "");
|
|
913
|
-
} else if (
|
|
1209
|
+
} else if (KEYWORDS.has(first)) {
|
|
914
1210
|
isWrapped = true;
|
|
915
1211
|
tokens.shift();
|
|
916
|
-
|
|
917
|
-
|
|
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);
|
|
918
1223
|
} else {
|
|
919
1224
|
break;
|
|
920
1225
|
}
|
|
@@ -928,6 +1233,131 @@ function parseCommand(text) {
|
|
|
928
1233
|
const [program = "", ...args] = tokens;
|
|
929
1234
|
return { program: program.split("/").pop() ?? "", args, assignments, isWrapped };
|
|
930
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
|
+
}
|
|
931
1361
|
function replacePieces(command, replacements) {
|
|
932
1362
|
let result = command;
|
|
933
1363
|
for (const [piece, text] of [...replacements].sort(([a], [b]) => b.start - a.start)) {
|
|
@@ -961,10 +1391,14 @@ var LOCK_KIND = /^(?:update|share|no|key)$/;
|
|
|
961
1391
|
var DIGITS = /\d/;
|
|
962
1392
|
var COMMAND_ENDS = ";";
|
|
963
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]/;
|
|
964
1398
|
var PRIVILEGED_SET = new Set(["role", "session", "transaction"]);
|
|
965
1399
|
var READ_STARTS = new Set(["select", "with", "values", "table", "show", "explain"]);
|
|
966
1400
|
var WRITE_WORDS = new Set(["insert", "update", "delete", "merge", "truncate", "drop", "alter", "create", "grant", "revoke", "into", "call", "vacuum", "lock", "refresh", "reindex", "cluster", "do"]);
|
|
967
|
-
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+)$/;
|
|
968
1402
|
function tokenize2(sql) {
|
|
969
1403
|
const tokens = [];
|
|
970
1404
|
let at = 0;
|
|
@@ -973,21 +1407,29 @@ function tokenize2(sql) {
|
|
|
973
1407
|
const next = sql.charAt(at + 1);
|
|
974
1408
|
if (SPACE.test(char)) {
|
|
975
1409
|
at++;
|
|
1410
|
+
} else if (char === NUL) {
|
|
1411
|
+
tokens.push({ kind: "punct", text: NUL });
|
|
1412
|
+
at++;
|
|
976
1413
|
} else if (char === "-" && next === "-") {
|
|
977
|
-
const end = sql.
|
|
978
|
-
|
|
979
|
-
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;
|
|
980
1416
|
} else if (char === "/" && next === "*" && sql.charAt(at + 2) === "!") {
|
|
981
1417
|
at += 3;
|
|
982
1418
|
while (DIGITS.test(sql.charAt(at)))
|
|
983
1419
|
at++;
|
|
984
1420
|
} else if (char === "/" && next === "*") {
|
|
985
1421
|
at = skipComment(sql, at);
|
|
986
|
-
} else if (char === "'"
|
|
987
|
-
const backslashes =
|
|
1422
|
+
} else if (char === "'") {
|
|
1423
|
+
const backslashes = ESCAPE_PREFIX.test(sql.charAt(at - 1)) && !WORD_PART.test(sql.charAt(at - 2));
|
|
988
1424
|
const end = skipQuoted(sql, at, char, backslashes);
|
|
989
1425
|
at = end ?? sql.length;
|
|
990
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;
|
|
991
1433
|
} else if (char === "$" && DOLLAR_TAG.test(sql.slice(at))) {
|
|
992
1434
|
const tag = DOLLAR_TAG.exec(sql.slice(at))?.[0] ?? "$$";
|
|
993
1435
|
const end = sql.indexOf(tag, at + tag.length);
|
|
@@ -1072,6 +1514,12 @@ function statementWrites(statement) {
|
|
|
1072
1514
|
for (const [index, token] of statement.entries()) {
|
|
1073
1515
|
if (isPunct(token, UNCLOSED))
|
|
1074
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}`);
|
|
1075
1523
|
if (token.kind !== "word")
|
|
1076
1524
|
continue;
|
|
1077
1525
|
if (SIDE_EFFECT_FUNCTIONS.test(token.text)) {
|
|
@@ -1104,23 +1552,104 @@ function writes(sql) {
|
|
|
1104
1552
|
var SQL_CLIENTS = new Set(["psql", "mysql", "mariadb", "pgcli", "mycli", "sqlite3"]);
|
|
1105
1553
|
var WRITING_TOOLS = new Set(["dropdb", "createdb", "pg_restore"]);
|
|
1106
1554
|
var CONSOLE_PROGRAMS = new Set(["php", "docker", "symfony", "console", "artisan"]);
|
|
1107
|
-
var
|
|
1108
|
-
var
|
|
1109
|
-
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"]);
|
|
1110
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;
|
|
1111
1568
|
var WRITING_CONSOLE = /^(?:doctrine:(?:migrations:(?:migrate|execute|rollup)|schema:(?:update|create|drop)|database:(?:create|drop)|fixtures:load)|migrate(?::\w+)?|db:(?:seed|wipe))$/;
|
|
1112
1569
|
var TEST_SCOPED = /(?:^|[^a-z])test/i;
|
|
1113
1570
|
var SHOT_STACK = /\.worktrees\/[^/\s]+\/shot(?:\/|$)|--shot(?:-|$)/;
|
|
1571
|
+
var DEVICE_PATH = /^\/(?:dev|proc|sys)\//;
|
|
1572
|
+
var STDIN_PATHS = new Set(["-", "/dev/stdin"]);
|
|
1114
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.";
|
|
1115
1575
|
function hasWrite(statement) {
|
|
1116
1576
|
return writes(statement).length > 0;
|
|
1117
1577
|
}
|
|
1118
1578
|
function programOf(word) {
|
|
1119
1579
|
return word.split("/").pop() ?? "";
|
|
1120
1580
|
}
|
|
1121
|
-
function
|
|
1122
|
-
|
|
1123
|
-
|
|
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);
|
|
1124
1653
|
}
|
|
1125
1654
|
async function sqlViolation(command, read) {
|
|
1126
1655
|
const pieces = splitCommand(command);
|
|
@@ -1130,23 +1659,27 @@ async function sqlViolation(command, read) {
|
|
|
1130
1659
|
const { program, args } = parseCommand(piece.command);
|
|
1131
1660
|
if (program === "cd")
|
|
1132
1661
|
inShotStack = SHOT_STACK.test(args[0] ?? "");
|
|
1133
|
-
|
|
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))
|
|
1134
1665
|
continue;
|
|
1135
1666
|
if (WRITING_TOOLS.has(program) && !words.some((word) => LIST_OPTIONS.has(word)))
|
|
1136
1667
|
return SQL_WRITE;
|
|
1137
1668
|
if (CONSOLE_PROGRAMS.has(program) && words.some((word) => WRITING_CONSOLE.test(word)))
|
|
1138
1669
|
return SQL_WRITE;
|
|
1139
|
-
|
|
1140
|
-
if (client === -1)
|
|
1670
|
+
if (input === null)
|
|
1141
1671
|
continue;
|
|
1142
|
-
const files = words.filter((_, i) => i > client && FILE_OPTIONS.has(words[i - 1] ?? ""));
|
|
1143
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;
|
|
1144
1678
|
const statements = [
|
|
1145
|
-
...
|
|
1146
|
-
piece.
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
...(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 ?? "")
|
|
1150
1683
|
];
|
|
1151
1684
|
if (statements.some(hasWrite))
|
|
1152
1685
|
return SQL_WRITE;
|
|
@@ -1155,32 +1688,130 @@ async function sqlViolation(command, read) {
|
|
|
1155
1688
|
}
|
|
1156
1689
|
|
|
1157
1690
|
// ../env/src/files.ts
|
|
1158
|
-
var ENV_FILE =
|
|
1691
|
+
var ENV_FILE = /^(?:\.env(?:\..+)?|.+\.env|\.envrc)$/;
|
|
1159
1692
|
var TEMPLATE = /\.(?:example|sample|template|dist|defaults)$/;
|
|
1693
|
+
var SKIPPED = new Set(["node_modules", ".git", "vendor"]);
|
|
1160
1694
|
|
|
1161
1695
|
// ../env/src/guard.ts
|
|
1162
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"]);
|
|
1163
1728
|
var ENV_DUMP = /(?:console\.\w+|JSON\.stringify|Object\.(?:entries|keys|values)|print)\(\s*process\.env\s*[,)]/;
|
|
1164
1729
|
var HARMLESS = new Set(["ev", "redact", "ls", "stat", "test", "[", "[[", "touch"]);
|
|
1165
1730
|
var GIT_HARMLESS = new Set(["check-ignore", "ls-files", "status"]);
|
|
1166
1731
|
var ENV_PRINTERS = new Set(["env", "printenv"]);
|
|
1167
1732
|
var DOCKER = new Set(["docker", "docker-compose"]);
|
|
1168
|
-
var
|
|
1733
|
+
var SHELLS2 = new Set(["sh", "bash", "zsh", "ash"]);
|
|
1169
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"]);
|
|
1170
1735
|
var EXEC_VALUE_OPTIONS = new Set(["-e", "--env", "--env-file", "-u", "--user", "-w", "--workdir", "--index", "--detach-keys"]);
|
|
1171
1736
|
var CONFIG_LISTS = new Set(["--services", "--volumes", "--profiles", "--images", "--networks", "--hash", "-q", "--quiet"]);
|
|
1172
1737
|
var FORMAT_OPTIONS = new Set(["-f", "--format"]);
|
|
1173
1738
|
var WHOLE_FORMAT = /Env|\{\{\s*json\s+\.\s*\}\}|^json$/;
|
|
1174
|
-
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 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`.';
|
|
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`.';
|
|
1175
1740
|
function isEnvFile(path) {
|
|
1176
1741
|
const name = path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf(":")) + 1);
|
|
1177
|
-
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}$`);
|
|
1178
1761
|
}
|
|
1179
|
-
function
|
|
1180
|
-
|
|
1762
|
+
function globReachesEnv(path) {
|
|
1763
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
1764
|
+
if (!name.startsWith(".") || !GLOB_CHAR.test(name))
|
|
1181
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) {
|
|
1182
1773
|
const bare = word.replace(REDIRECT, "");
|
|
1183
|
-
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] ?? ""))));
|
|
1184
1815
|
}
|
|
1185
1816
|
function firstOperand(words, valueOptions) {
|
|
1186
1817
|
for (let i = 0;i < words.length; i++) {
|
|
@@ -1193,7 +1824,7 @@ function firstOperand(words, valueOptions) {
|
|
|
1193
1824
|
return words.length;
|
|
1194
1825
|
}
|
|
1195
1826
|
function printsEnv([program = "", ...args]) {
|
|
1196
|
-
if (
|
|
1827
|
+
if (SHELLS2.has(program) && args[0] === "-c")
|
|
1197
1828
|
return splitCommand(args[1] ?? "").some((piece) => ENV_PRINTERS.has(tokenize(piece.command)[0] ?? ""));
|
|
1198
1829
|
return ENV_PRINTERS.has(program);
|
|
1199
1830
|
}
|
|
@@ -1215,24 +1846,79 @@ function dumpsContainerEnv(program, args) {
|
|
|
1215
1846
|
const format = after.find((_, i) => FORMAT_OPTIONS.has(after[i - 1] ?? "")) ?? after.find((word) => word.startsWith("--format="))?.slice("--format=".length);
|
|
1216
1847
|
return format === undefined || WHOLE_FORMAT.test(format);
|
|
1217
1848
|
}
|
|
1218
|
-
function readsEnv(piece) {
|
|
1849
|
+
function readsEnv(piece, variables) {
|
|
1219
1850
|
const { program, args } = parseCommand(piece.command);
|
|
1220
1851
|
const found = words(piece.command);
|
|
1221
1852
|
if (program === "" || HARMLESS.has(program) || program === "git" && GIT_HARMLESS.has(args[0] ?? ""))
|
|
1222
1853
|
return false;
|
|
1223
|
-
|
|
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)));
|
|
1858
|
+
}
|
|
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));
|
|
1224
1908
|
}
|
|
1225
|
-
function touchesEnv(command) {
|
|
1909
|
+
function touchesEnv(command, options = {}) {
|
|
1226
1910
|
if (ENV_DUMP.test(command))
|
|
1227
1911
|
return true;
|
|
1228
|
-
if (substitutions(command).some(touchesEnv))
|
|
1912
|
+
if (substitutions(command).some((each) => touchesEnv(each, options)))
|
|
1229
1913
|
return true;
|
|
1230
1914
|
const pieces = splitCommand(command);
|
|
1915
|
+
const variables = variablesOf(command);
|
|
1231
1916
|
return pieces.some((piece, index) => {
|
|
1232
|
-
if (readsEnv(piece))
|
|
1233
|
-
return true;
|
|
1234
1917
|
const { program, args } = parseCommand(piece.command);
|
|
1235
|
-
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)
|
|
1236
1922
|
return false;
|
|
1237
1923
|
for (let at = index;pieces[at]?.separator === "|"; at++) {
|
|
1238
1924
|
if (parseCommand(pieces[at + 1]?.command ?? "").program === "redact")
|
|
@@ -1243,17 +1929,19 @@ function touchesEnv(command) {
|
|
|
1243
1929
|
}
|
|
1244
1930
|
|
|
1245
1931
|
// ../git/src/message.ts
|
|
1246
|
-
var CONVENTIONAL = /^
|
|
1932
|
+
var CONVENTIONAL = /^(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\([^()\n]+\))?!?: \S/;
|
|
1933
|
+
var MAX_TITLE = 100;
|
|
1247
1934
|
var GIT_MADE = /^(?:Merge |Revert "|fixup! |squash! |amend! )/;
|
|
1248
1935
|
var TICKET = /^(?:[\w.-]+\/[\w.-]+)?#\d+ - /;
|
|
1249
1936
|
var BREAKING = /^BREAKING[ -]CHANGE: /;
|
|
1250
1937
|
var BREAKING_TITLE = /^[a-z]+(?:\([^()\n]+\))?!: /;
|
|
1251
|
-
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;
|
|
1252
1939
|
var BULLET = /^\s*(?:[-*+]|\d+[.)])\s/;
|
|
1253
1940
|
var SENTENCE_END = /[.!?:;)\]`'"]$/;
|
|
1254
1941
|
var LOWER_START = /^\p{Ll}/u;
|
|
1255
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.';
|
|
1256
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}`;
|
|
1257
1945
|
var TITLE_LINE = `The title takes one line and a blank line follows it. ${RULE}`;
|
|
1258
1946
|
var TRAILERS = `No trailers, ever: no Co-Authored-By, Signed-off-by or "Generated with" line, no --signoff or --trailer. ${RULE}`;
|
|
1259
1947
|
var BULLETS = `The body names what changed in a line or two, never a bullet list narrating the diff. ${RULE}`;
|
|
@@ -1279,20 +1967,26 @@ function commitMessageViolation(message) {
|
|
|
1279
1967
|
return TITLE;
|
|
1280
1968
|
if (second !== undefined && second !== "")
|
|
1281
1969
|
return TITLE_LINE;
|
|
1282
|
-
if (
|
|
1970
|
+
if (!GIT_MADE.test(title) && title.length > MAX_TITLE)
|
|
1971
|
+
return TITLE_TOO_LONG;
|
|
1972
|
+
if (BREAKING_TITLE.test(title))
|
|
1283
1973
|
return null;
|
|
1284
|
-
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);
|
|
1285
1977
|
if (descriptions.some((line) => BULLET.test(line)))
|
|
1286
1978
|
return BULLETS;
|
|
1287
1979
|
if (descriptions.length > 2)
|
|
1288
1980
|
return TOO_LONG;
|
|
1289
|
-
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] ?? ""));
|
|
1290
1982
|
return isWrapped ? WRAPPED : null;
|
|
1291
1983
|
}
|
|
1292
1984
|
|
|
1293
1985
|
// ../out/src/wrap.ts
|
|
1294
|
-
var
|
|
1295
|
-
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*)?$/;
|
|
1296
1990
|
var BUN_SCRIPT = /^(?:build|check|lint|typecheck|types|test|ci|verify|compile)(?::|$)/;
|
|
1297
1991
|
var BUN_TOOLS = new Set(["tsc", "tsgo", "eslint", "keslint", "vitest", "jest", "prettier", "playwright", "biome", "vue-tsc"]);
|
|
1298
1992
|
var BUN_SUBCOMMANDS = new Set(["install", "i", "add", "remove", "rm", "update", "test", "check", "lint", "build"]);
|
|
@@ -1328,9 +2022,9 @@ var RULES = {
|
|
|
1328
2022
|
"php-cs-fixer": always,
|
|
1329
2023
|
rector: always,
|
|
1330
2024
|
php: (args) => listed(afterOptions(args, PHP_VALUE_OPTIONS)),
|
|
1331
|
-
make:
|
|
2025
|
+
make: ([first]) => first === undefined || !LONG_RUNNING.test(first),
|
|
1332
2026
|
check: always,
|
|
1333
|
-
just: ([first]) => first !== undefined && !first.startsWith("-"),
|
|
2027
|
+
just: ([first]) => first !== undefined && !first.startsWith("-") && !LONG_RUNNING.test(first),
|
|
1334
2028
|
composer: ([sub]) => sub === "install" || sub === "update" || sub === "require",
|
|
1335
2029
|
pip: ([sub]) => sub === "install",
|
|
1336
2030
|
cargo: ([sub]) => ["build", "test", "check", "clippy"].includes(sub ?? ""),
|
|
@@ -1388,14 +2082,14 @@ function withoutFilters(command) {
|
|
|
1388
2082
|
return rest;
|
|
1389
2083
|
}
|
|
1390
2084
|
function listed(words) {
|
|
1391
|
-
const start = words.findIndex((word) => !
|
|
2085
|
+
const start = words.findIndex((word) => !ASSIGNMENT3.test(word));
|
|
1392
2086
|
const path = words[start];
|
|
1393
2087
|
if (path === undefined)
|
|
1394
2088
|
return false;
|
|
1395
2089
|
return RULES[path.slice(path.lastIndexOf("/") + 1)]?.(words.slice(start + 1)) ?? false;
|
|
1396
2090
|
}
|
|
1397
2091
|
function listedSegment(segment) {
|
|
1398
|
-
return !EXCLUDED.test(segment) && listed(tokenize(segment));
|
|
2092
|
+
return !EXCLUDED.test(segment) && !SHORT_WATCH.test(segment) && listed(tokenize(segment));
|
|
1399
2093
|
}
|
|
1400
2094
|
function isCd(segment) {
|
|
1401
2095
|
return segment === "cd" || segment.startsWith("cd ");
|
|
@@ -1406,14 +2100,14 @@ function shouldWrap(command) {
|
|
|
1406
2100
|
return false;
|
|
1407
2101
|
return segments.every((segment) => isCd(segment) || listedSegment(segment));
|
|
1408
2102
|
}
|
|
1409
|
-
function
|
|
2103
|
+
function quoted2(value) {
|
|
1410
2104
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1411
2105
|
}
|
|
1412
2106
|
function wrap(command) {
|
|
1413
2107
|
const trimmed = withoutFilters(command.trim());
|
|
1414
|
-
if (scan(trimmed).segments.length <= 1 && !
|
|
2108
|
+
if (scan(trimmed).segments.length <= 1 && !ASSIGNMENT3.test(trimmed))
|
|
1415
2109
|
return `out -- ${trimmed}`;
|
|
1416
|
-
return `out -- sh -c ${
|
|
2110
|
+
return `out -- sh -c ${quoted2(trimmed)}`;
|
|
1417
2111
|
}
|
|
1418
2112
|
|
|
1419
2113
|
// ../pkg/src/added.ts
|
|
@@ -1422,10 +2116,18 @@ var BARE_REDIRECT = /^\d*(?:[<>]|>>)$/;
|
|
|
1422
2116
|
var ADDS = {
|
|
1423
2117
|
bun: new Set(["add", "a", "install", "i"]),
|
|
1424
2118
|
npm: new Set(["install", "i", "add", "in", "isntall"]),
|
|
1425
|
-
pnpm: new Set(["add"]),
|
|
2119
|
+
pnpm: new Set(["add", "i", "install"]),
|
|
1426
2120
|
yarn: new Set(["add"])
|
|
1427
2121
|
};
|
|
1428
|
-
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]);
|
|
1429
2131
|
var NOT_REGISTRY = /^(?:[./~]|file:|link:|portal:|workspace:|git[+:]|github:|gitlab:|bitbucket:|https?:)|\.tgz$|^[\w.-]+\/[\w.-]+(?:#.*)?$/;
|
|
1430
2132
|
var NPM_ALIAS = /^(?:@[^/]+\/)?[^@]+@npm:(.+)$/;
|
|
1431
2133
|
var PUBLISH = /\b(?:ft|cli\.ts)\s+release\b|\b(?:bun|npm|pnpm|yarn)\s+publish\b|\bjust\s+(?:patch|minor|major)\b/;
|
|
@@ -1435,16 +2137,38 @@ function addedPackages(command) {
|
|
|
1435
2137
|
published ||= PUBLISH.test(piece.command);
|
|
1436
2138
|
const { program, args } = parseCommand(piece.command);
|
|
1437
2139
|
const [sub = "", ...rest] = args;
|
|
1438
|
-
if (published
|
|
2140
|
+
if (published)
|
|
1439
2141
|
return [];
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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))
|
|
2147
|
+
return [];
|
|
2148
|
+
return rest.flatMap((word, index) => isSkipped(word, rest[index - 1] ?? "") ? [] : registryName(word));
|
|
1446
2149
|
});
|
|
1447
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
|
+
}
|
|
1448
2172
|
|
|
1449
2173
|
// ../pkg/src/pin.ts
|
|
1450
2174
|
var LOCKFILES = [
|
|
@@ -1573,11 +2297,16 @@ var COMMIT_VALUE_LONGS = new Set(["--message", "--file", "--reuse-message", "--r
|
|
|
1573
2297
|
var SHORT_CLUSTER = /^-[A-Z]+$/i;
|
|
1574
2298
|
var STAGES_ALL = /[Au]/;
|
|
1575
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"]);
|
|
1576
2304
|
var STAGE_BY_PATH = `Stage by path: the tree carries unrelated work and peer sessions edit it live. ${USE_GC}`;
|
|
1577
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`.";
|
|
1578
2306
|
var ADD_ALL = `\`git add\` over the whole tree is refused. ${STAGE_BY_PATH}`;
|
|
1579
2307
|
var COMMIT_ALL = `\`git commit -a\` stages the whole tree and is refused. ${STAGE_BY_PATH}`;
|
|
1580
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.";
|
|
1581
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`.";
|
|
1582
2311
|
function subcommandOf(args) {
|
|
1583
2312
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -1589,6 +2318,35 @@ function subcommandOf(args) {
|
|
|
1589
2318
|
}
|
|
1590
2319
|
return { name: "", rest: [] };
|
|
1591
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
|
+
}
|
|
1592
2350
|
function commitsAll(args) {
|
|
1593
2351
|
for (let i = 0;i < args.length; i++) {
|
|
1594
2352
|
const arg = args[i] ?? "";
|
|
@@ -1616,13 +2374,19 @@ function commitsAll(args) {
|
|
|
1616
2374
|
}
|
|
1617
2375
|
function gitViolation(command, isPushAsked = false) {
|
|
1618
2376
|
for (const piece of splitCommand(command)) {
|
|
1619
|
-
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;
|
|
1620
2380
|
if (program !== "git")
|
|
1621
2381
|
continue;
|
|
2382
|
+
if (overridesConfig(args))
|
|
2383
|
+
return CONFIG_OVERRIDE;
|
|
1622
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;
|
|
1623
2387
|
if (name === "stash" && !READ_ONLY_STASH.has(rest[0] ?? ""))
|
|
1624
2388
|
return STASH;
|
|
1625
|
-
if (name === "add" && rest.some(
|
|
2389
|
+
if (name === "add" && rest.some(isWholeTree))
|
|
1626
2390
|
return ADD_ALL;
|
|
1627
2391
|
if (name === "commit" && commitsAll(rest))
|
|
1628
2392
|
return COMMIT_ALL;
|
|
@@ -1723,19 +2487,63 @@ function inlineMessage(parts) {
|
|
|
1723
2487
|
}
|
|
1724
2488
|
|
|
1725
2489
|
// src/remote.ts
|
|
1726
|
-
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"]);
|
|
1727
2494
|
var RSYNC_REMOTE = /^(?:[\w.-]+@)?[\w.-]{2,}:/;
|
|
1728
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)?=/;
|
|
1729
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
|
+
}
|
|
1730
2529
|
function isRemote(text) {
|
|
1731
2530
|
const { program, args, assignments } = parseCommand(text);
|
|
2531
|
+
const declared = DECLARERS2.has(program) ? args : [];
|
|
1732
2532
|
if (SHELL_PROGRAMS.has(program))
|
|
1733
2533
|
return true;
|
|
2534
|
+
if (SOCKET_PROGRAMS.has(program))
|
|
2535
|
+
return isSocketRemote(args);
|
|
1734
2536
|
if (program === "rsync")
|
|
1735
2537
|
return args.some((arg) => !arg.startsWith("-") && RSYNC_REMOTE.test(arg));
|
|
1736
|
-
if (
|
|
2538
|
+
if (program === "kubectl")
|
|
2539
|
+
return args.some((arg) => KUBECTL_SERVER.test(arg));
|
|
2540
|
+
if ([...assignments, ...declared].some((assignment) => REMOTE_VARIABLE.test(assignment)))
|
|
1737
2541
|
return true;
|
|
1738
|
-
|
|
2542
|
+
if ([...assignments, ...declared].some((assignment) => assignment.startsWith("DOCKER_HOST=") && SSH_URL.test(assignment.slice("DOCKER_HOST=".length))))
|
|
2543
|
+
return true;
|
|
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));
|
|
1739
2547
|
}
|
|
1740
2548
|
function remoteViolation(command) {
|
|
1741
2549
|
return splitCommand(command).some((piece) => isRemote(piece.command)) ? REMOTE : null;
|
|
@@ -2066,6 +2874,7 @@ function replaceViolation(command) {
|
|
|
2066
2874
|
}
|
|
2067
2875
|
|
|
2068
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.";
|
|
2069
2878
|
function absolute(path, cwd) {
|
|
2070
2879
|
return path.startsWith("/") ? path : `${cwd}/${path}`;
|
|
2071
2880
|
}
|
|
@@ -2081,6 +2890,8 @@ function commitViolation(command, { cwd, read }) {
|
|
|
2081
2890
|
message = inlineMessage(source.parts);
|
|
2082
2891
|
else if (file !== "-")
|
|
2083
2892
|
message = read(absolute(file, cwd));
|
|
2893
|
+
if (message === null && (source.parts.length > 0 || file !== null))
|
|
2894
|
+
return UNREADABLE_MESSAGE;
|
|
2084
2895
|
const violation = message === null ? null : commitMessageViolation(message);
|
|
2085
2896
|
if (violation !== null)
|
|
2086
2897
|
return violation;
|
|
@@ -2096,9 +2907,6 @@ function pinnedManagers(start, exists) {
|
|
|
2096
2907
|
return null;
|
|
2097
2908
|
}
|
|
2098
2909
|
}
|
|
2099
|
-
function commandsRun(command) {
|
|
2100
|
-
return [command, ...substitutions(command).flatMap(commandsRun)];
|
|
2101
|
-
}
|
|
2102
2910
|
function mismatches(command, { cwd, home, exists }) {
|
|
2103
2911
|
return installRequests(command, cwd, home).flatMap((request) => {
|
|
2104
2912
|
const mismatch = mismatchFor(request, pinnedManagers(request.dir, exists));
|
|
@@ -2114,13 +2922,22 @@ async function checkCommand(command, context) {
|
|
|
2114
2922
|
};
|
|
2115
2923
|
const run = commandsRun(command);
|
|
2116
2924
|
const first = (messages) => messages.find((message) => message !== null) ?? null;
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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))));
|
|
2120
2937
|
add("commit", commit === null ? null : `${commit} ${USE_GC}`);
|
|
2121
|
-
add("sql", first(await Promise.all(run.map(async (each) => sqlViolation(each,
|
|
2122
|
-
add("env", touchesEnv(
|
|
2123
|
-
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);
|
|
2124
2941
|
const found = mismatches(command, context);
|
|
2125
2942
|
const refused = found.find((mismatch) => mismatch.rewrite === null || !context.canRewrite);
|
|
2126
2943
|
const rewrite = refused?.rewrite ?? null;
|
|
@@ -2133,10 +2950,10 @@ async function checkCommand(command, context) {
|
|
|
2133
2950
|
}
|
|
2134
2951
|
if (context.canRewrite && context.canWrap && shouldWrap(rewritten))
|
|
2135
2952
|
rewritten = wrap(rewritten);
|
|
2136
|
-
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))] };
|
|
2137
2954
|
}
|
|
2138
2955
|
function checkPath(path) {
|
|
2139
|
-
return
|
|
2956
|
+
return reachesEnvFile(path) ? { rule: "env", message: USE_EV } : null;
|
|
2140
2957
|
}
|
|
2141
2958
|
|
|
2142
2959
|
// src/bins.ts
|
|
@@ -2145,7 +2962,7 @@ var BINS = {
|
|
|
2145
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.",
|
|
2146
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.",
|
|
2147
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`.',
|
|
2148
|
-
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.",
|
|
2149
2966
|
redact: "`redact`: masks secrets in piped output, `<cmd> | redact`.",
|
|
2150
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.",
|
|
2151
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.",
|
|
@@ -2175,9 +2992,21 @@ ${lines.join(`
|
|
|
2175
2992
|
|
|
2176
2993
|
// src/hook.ts
|
|
2177
2994
|
var EVENTS = ["pre-tool-use", "user-prompt-submit", "session-start"];
|
|
2178
|
-
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]+/;
|
|
2179
2999
|
var SESSION_ID = /^[\w-]+$/;
|
|
2180
|
-
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
|
+
}
|
|
2181
3010
|
function pushFile(sessionId, { stateDir }) {
|
|
2182
3011
|
return sessionId !== undefined && SESSION_ID.test(sessionId) ? `${stateDir}/push-asked/${sessionId}` : null;
|
|
2183
3012
|
}
|
|
@@ -2199,25 +3028,36 @@ async function bashCall(command, input, machine) {
|
|
|
2199
3028
|
canWrap: false,
|
|
2200
3029
|
canSub: machine.has("sub"),
|
|
2201
3030
|
read: machine.read,
|
|
2202
|
-
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
|
+
`)
|
|
2203
3036
|
});
|
|
2204
3037
|
if (verdict.violations.length > 0 || verdict.packages.length === 0 || !machine.has("pkg"))
|
|
2205
3038
|
return deny(verdict.violations);
|
|
2206
3039
|
const checked = await machine.run(["pkg", "check", ...verdict.packages], cwd);
|
|
2207
|
-
|
|
2208
|
-
|
|
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);
|
|
2209
3049
|
}
|
|
2210
3050
|
function preToolUse(input, machine) {
|
|
2211
3051
|
const { tool_name: tool, tool_input: given = {} } = input;
|
|
2212
3052
|
if (tool === "Bash" && typeof given.command === "string")
|
|
2213
3053
|
return bashCall(given.command, input, machine);
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
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));
|
|
2217
3057
|
}
|
|
2218
3058
|
function userPromptSubmit(input, machine) {
|
|
2219
3059
|
const file = pushFile(input.session_id, machine);
|
|
2220
|
-
if (file !== null &&
|
|
3060
|
+
if (file !== null && asksForPush(input.prompt ?? ""))
|
|
2221
3061
|
machine.write(file, "");
|
|
2222
3062
|
else if (file !== null)
|
|
2223
3063
|
machine.remove(file);
|
|
@@ -2258,13 +3098,15 @@ var pathOptions = {
|
|
|
2258
3098
|
var hookUsage = `guard hook <${EVENTS.join("|")}>
|
|
2259
3099
|
|
|
2260
3100
|
Answers a Claude Code hook: reads the event JSON on stdin and prints the reply JSON, nothing when the call goes ahead untouched.
|
|
2261
|
-
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.
|
|
2262
3102
|
A failure exits 1, which Claude Code reports without blocking the call.
|
|
2263
3103
|
|
|
2264
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;
|
|
2265
3106
|
function readText(path) {
|
|
2266
3107
|
try {
|
|
2267
|
-
|
|
3108
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
3109
|
+
return stats?.isFile() === true && stats.size <= MAX_READ ? readFileSync(path, "utf8") : null;
|
|
2268
3110
|
} catch {
|
|
2269
3111
|
return null;
|
|
2270
3112
|
}
|
|
@@ -2283,7 +3125,7 @@ ${checkUsage}`);
|
|
|
2283
3125
|
const given = positionals[0] ?? "-";
|
|
2284
3126
|
const command = given === "-" ? await readInput("-", { noStdin: "give the command, or pipe it on stdin" }) : given;
|
|
2285
3127
|
const context = {
|
|
2286
|
-
cwd: values.cwd ??
|
|
3128
|
+
cwd: values.cwd ?? process7.cwd(),
|
|
2287
3129
|
home: homedir(),
|
|
2288
3130
|
isPushAsked: values["push-asked"] === true,
|
|
2289
3131
|
canRewrite: true,
|
|
@@ -2317,9 +3159,10 @@ ${pathUsage}`);
|
|
|
2317
3159
|
}
|
|
2318
3160
|
function machine() {
|
|
2319
3161
|
const home = homedir();
|
|
3162
|
+
const stateDir = `${process7.env.XDG_STATE_HOME ?? `${home}/.local/state`}/guard`;
|
|
2320
3163
|
return {
|
|
2321
3164
|
home,
|
|
2322
|
-
stateDir
|
|
3165
|
+
stateDir,
|
|
2323
3166
|
has: (bin) => Bun.which(bin) !== null,
|
|
2324
3167
|
run: async (argv, cwd) => runCommand(argv, { cwd }),
|
|
2325
3168
|
read: readText,
|
|
@@ -2328,7 +3171,13 @@ function machine() {
|
|
|
2328
3171
|
mkdirSync(dirname(path), { recursive: true });
|
|
2329
3172
|
writeFileSync(path, text);
|
|
2330
3173
|
},
|
|
2331
|
-
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
|
+
}
|
|
2332
3181
|
};
|
|
2333
3182
|
}
|
|
2334
3183
|
async function hookCommand(args) {
|
|
@@ -2343,7 +3192,7 @@ ${hookUsage}`, 1);
|
|
|
2343
3192
|
console.log(JSON.stringify(reply));
|
|
2344
3193
|
return 0;
|
|
2345
3194
|
}
|
|
2346
|
-
|
|
3195
|
+
process7.exitCode = await runCli({
|
|
2347
3196
|
name: "guard",
|
|
2348
3197
|
version,
|
|
2349
3198
|
about: "the safety rules a Claude Code Bash call is held to, each refusal naming the route",
|