@brainervirus/workit-core 8.16.0 → 8.17.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.
@@ -544,7 +544,29 @@ export function rawInvocations(
544
544
  /** The invocations of the last few commands parsed (pure: same text, same answer). */
545
545
  const RECENT = new Map<string, readonly RawInvocation[]>();
546
546
 
547
- function walk(command: string, dialect: ShellDialect, depth: number): RawInvocation[] {
547
+ /**
548
+ * Every command line a shell command runs, past the same wrappers, nesting
549
+ * and chains rawInvocations reads (`bash -c '…'`, `flock f -c '…'`, `xargs`,
550
+ * `timeout 30`, env prefixes): the words after the prefixes, outermost first.
551
+ */
552
+ export function commandLines(command: string, dialect: ShellDialect = "posix"): string[][] {
553
+ const lines: string[][] = [];
554
+ try {
555
+ walk(command, dialect, 0, lines);
556
+ } catch (error) {
557
+ throw error instanceof ShellAnalysisError
558
+ ? error
559
+ : new ShellAnalysisError(error instanceof Error ? error.message : String(error));
560
+ }
561
+ return lines;
562
+ }
563
+
564
+ function walk(
565
+ command: string,
566
+ dialect: ShellDialect,
567
+ depth: number,
568
+ seen?: string[][],
569
+ ): RawInvocation[] {
548
570
  const found: RawInvocation[] = [];
549
571
  let dir: Dir = null;
550
572
  const groups: Array<{ dir: Dir; stack: Dir[] }> = [];
@@ -556,7 +578,10 @@ function walk(command: string, dialect: ShellDialect, depth: number): RawInvocat
556
578
  const sessionConfig: string[] = [];
557
579
  const nested = (script: string | null | undefined, inner: ShellDialect) => {
558
580
  if (!script || depth >= MAX_DEPTH) return;
559
- for (const invocation of rawInvocations(script, inner, depth + 1))
581
+ const inside = seen
582
+ ? walk(script, inner, depth + 1, seen)
583
+ : rawInvocations(script, inner, depth + 1);
584
+ for (const invocation of inside)
560
585
  found.push({ ...invocation, dir: rebase(dir, invocation.dir) });
561
586
  };
562
587
  const visit = (input: string[], stdin: string | undefined, hops: number) => {
@@ -567,9 +592,18 @@ function walk(command: string, dialect: ShellDialect, depth: number): RawInvocat
567
592
  const { words, env } = stripPrefixes(line);
568
593
  const name = commandName(words[0]);
569
594
  if (!name) return;
595
+ seen?.push(words);
596
+ // `flock <file> -c '…'`: the wrapper's script.
597
+ if (name === "-c" || name === "--command") return nested(words[1], "posix");
570
598
  const runs = (lines: string[][]) => {
571
599
  if (hops < MAX_DEPTH) for (const next of lines) visit(next, undefined, hops + 1);
572
600
  };
601
+ // The gates judge what `workit check … -- <cmd>` runs as if it ran raw.
602
+ // (The heavy-command reader, `seen`, stops at workit: a wrapped command
603
+ // already runs under the lease.)
604
+ const wrapped = seen ? null : workitCheckTail(words);
605
+ if (wrapped)
606
+ return "script" in wrapped ? nested(wrapped.script, "posix") : runs([wrapped.argv]);
573
607
  if (CD.has(name)) {
574
608
  const target = cdTarget(words);
575
609
  dir = target === null ? os.homedir() : joinDir(dir, target);
@@ -659,6 +693,74 @@ const LAUNCHERS = new Set([
659
693
  "command",
660
694
  ]);
661
695
  const WORKIT_BIN = /(?:^|[\\/])workit(?:\.exe)?$|^@brainervirus\/workit-cli$/;
696
+ const WORKIT_PACKAGE = /^@brainervirus\/workit-cli(?:@\S*)?$/;
697
+ const PACKAGE_RUNNERS = new Set(["npx", "bunx", "pnpx"]);
698
+
699
+ /**
700
+ * What a `workit check [flags] -- <cmd…>` (or `workit lock run -- <cmd…>`,
701
+ * also through `npx @brainervirus/workit-cli`) runs: the script of `--shell`,
702
+ * else the argv. Null for any other command.
703
+ */
704
+ export function workitCheckTail(
705
+ words: readonly string[],
706
+ ): { script: string } | { argv: string[] } | null {
707
+ let at = 0;
708
+ if (PACKAGE_RUNNERS.has(commandName(words[0]))) {
709
+ at = words.findIndex((word, index) => index > 0 && !word.startsWith("-"));
710
+ if (at < 0 || !WORKIT_PACKAGE.test(words[at])) return null;
711
+ } else if (!/^workit(?:\.exe|\.cmd)?$/.test(commandName(words[0]))) return null;
712
+ const args = words.slice(at + 1);
713
+ if (args[0] !== "check" && !(args[0] === "lock" && args[1] === "run")) return null;
714
+ const split = args.indexOf("--");
715
+ const tail = split < 0 ? [] : args.slice(split + 1);
716
+ if (!tail.length) return null;
717
+ return args.slice(0, split).includes("--shell") ? { script: tail.join(" ") } : { argv: tail };
718
+ }
719
+
720
+ /**
721
+ * The one command `command` runs after its wrappers (env prefixes, `timeout`,
722
+ * `nice`, `flock`, `bash -c '<one command>'`), or null when it is a chain,
723
+ * pipe, redirect, substitution, group or heredoc anywhere.
724
+ */
725
+ export function simpleCommand(
726
+ command: string,
727
+ dialect: ShellDialect = "posix",
728
+ depth = 0,
729
+ ): string[] | null {
730
+ let segments: Segment[];
731
+ try {
732
+ segments = segmentsOf(command, dialect);
733
+ } catch {
734
+ return null;
735
+ }
736
+ if (segments.length !== 1) return null;
737
+ const [segment] = segments;
738
+ if (
739
+ segment.group ||
740
+ segment.pipe ||
741
+ segment.unanalyzable ||
742
+ segment.redirects.length ||
743
+ segment.subs?.length ||
744
+ segment.stdin?.length
745
+ )
746
+ return null;
747
+ const { words } = stripPrefixes(segment.words);
748
+ const name = commandName(words[0]);
749
+ if (!name || depth >= MAX_DEPTH) return null;
750
+ if (SHELLS.has(name)) {
751
+ const script = shellScript(words, undefined);
752
+ return script ? simpleCommand(script, "posix", depth + 1) : null;
753
+ }
754
+ if (name === "-c" || name === "--command")
755
+ return words[1] ? simpleCommand(words[1], "posix", depth + 1) : null;
756
+ if (
757
+ EVALS.has(name) ||
758
+ POWERSHELLS.has(name) ||
759
+ ["xargs", "find", "parallel", "script", "cmd", "source", "."].includes(name)
760
+ )
761
+ return null;
762
+ return words;
763
+ }
662
764
 
663
765
  /**
664
766
  * The arguments of every workit CLI invocation in a shell command: past env
@@ -23,6 +23,7 @@ import type { HookDecision } from "./protocol";
23
23
  import {
24
24
  inWorkitWorkspace,
25
25
  rawInvocations,
26
+ workitCheckTail,
26
27
  workitInvocations,
27
28
  type RawInvocation,
28
29
  } from "./raw-git";
@@ -70,6 +71,28 @@ const exempt = (cwd: string, root: string, file: string, plans: ReadonlySet<stri
70
71
  const PREFIXES = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
71
72
  const operands = (words: string[]) => words.filter((item) => !item.startsWith("-"));
72
73
 
74
+ const SCRIPT_SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
75
+
76
+ /**
77
+ * The script `workit check … -- <cmd>` runs, so the gates judge it as if it
78
+ * ran raw: the `--shell` string, a `bash -c` script, or the argv. Other
79
+ * workit verbs stay exempt (null).
80
+ */
81
+ const wrappedScript = (input: string[]): string | null => {
82
+ let words = input;
83
+ while (words.length && (PREFIXES.has(words[0]) || /^\w+=/.test(words[0]))) words = words.slice(1);
84
+ const tail = workitCheckTail(words);
85
+ if (!tail) return null;
86
+ if ("script" in tail) return tail.script;
87
+ const [shell, flag, script] = tail.argv;
88
+ if (
89
+ SCRIPT_SHELLS.has((shell.split(/[\\/]/).at(-1) ?? "").toLowerCase()) &&
90
+ /^-[a-z]*c$/.test(flag ?? "")
91
+ )
92
+ return script ?? null;
93
+ return tail.argv.map(quoteWord).join(" ");
94
+ };
95
+
73
96
  /** The files one simple command edits: [] none, null unknown. */
74
97
  const editsOf = (input: string[]): string[] | null => {
75
98
  let words = input;
@@ -149,10 +172,19 @@ const editsOf = (input: string[]): string[] | null => {
149
172
  * known, else null. Interpreters and formatters are not detected; Workit's
150
173
  * own commands never count.
151
174
  */
152
- export const shellWrites = (command: string): { writes: boolean; targets: string[] | null } => {
175
+ export const shellWrites = (
176
+ command: string,
177
+ depth = 0,
178
+ ): { writes: boolean; targets: string[] | null } => {
153
179
  const targets: string[] = [];
154
180
  let unknown = false;
155
181
  for (const segment of segmentsOf(command)) {
182
+ const inner = depth < 4 ? wrappedScript(segment.words) : null;
183
+ if (inner !== null) {
184
+ const nested = shellWrites(inner, depth + 1);
185
+ if (nested.writes && nested.targets === null) unknown = true;
186
+ else if (nested.targets) targets.push(...nested.targets);
187
+ }
156
188
  targets.push(
157
189
  ...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)),
158
190
  );
@@ -309,7 +341,12 @@ const quoteWord = (word: string): string =>
309
341
  * directory that segment runs in (`cd`, `pushd`/`popd`, `( … )` groups). A
310
342
  * relative target in an unknown directory is skipped.
311
343
  */
312
- const segmentTargets = (cwd: string, command: string, dialect: ShellDialect): string[] => {
344
+ const segmentTargets = (
345
+ cwd: string,
346
+ command: string,
347
+ dialect: ShellDialect,
348
+ depth = 0,
349
+ ): string[] => {
313
350
  const targets: string[] = [];
314
351
  let dir: string | undefined = cwd;
315
352
  let stack: Array<string | undefined> = [];
@@ -342,6 +379,9 @@ const segmentTargets = (cwd: string, command: string, dialect: ShellDialect): st
342
379
  dir = stack.length ? stack.pop() : undefined;
343
380
  continue;
344
381
  }
382
+ const inner = depth < 4 ? wrappedScript(words) : null;
383
+ if (inner !== null && dir !== undefined)
384
+ targets.push(...segmentTargets(dir, inner, "posix", depth + 1));
345
385
  for (const file of segment.redirects)
346
386
  if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
347
387
  const resolved = at(file);