tamperward 2.24.4 → 2.24.6

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +171 -39
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1536,11 +1536,86 @@ function gitSubcommand(toks) {
1536
1536
  }
1537
1537
  return null;
1538
1538
  }
1539
- var GIT_OPT_WITH_ARG;
1539
+ function shellWord(t) {
1540
+ let out3 = "";
1541
+ let single = false;
1542
+ let double = false;
1543
+ for (let i = 0; i < t.length; i++) {
1544
+ const ch = t[i];
1545
+ if (single) {
1546
+ if (ch === "'") single = false;
1547
+ else out3 += ch;
1548
+ continue;
1549
+ }
1550
+ if (ch === "\\" && i + 1 < t.length && (!double || /["\\$`]/.test(t[i + 1]))) {
1551
+ out3 += t[++i];
1552
+ continue;
1553
+ }
1554
+ if (ch === "'" && !double) {
1555
+ single = true;
1556
+ continue;
1557
+ }
1558
+ if (ch === '"') {
1559
+ double = !double;
1560
+ continue;
1561
+ }
1562
+ out3 += ch;
1563
+ }
1564
+ return out3;
1565
+ }
1566
+ function expandBraces(t) {
1567
+ const m = t.match(/^([^{]*)\{([^{}]*)\}(.*)$/);
1568
+ if (!m) return [t];
1569
+ return m[2].split(",").flatMap((alt) => expandBraces(`${m[1]}${alt}${m[3]}`));
1570
+ }
1571
+ function globToRegExp(pattern) {
1572
+ const classes = [];
1573
+ const src = pattern.replace(/^\.\//, "").replace(/\[([^\][]*)\]/g, (_, body) => {
1574
+ classes.push(`[${body.replace(/^!/, "^").replace(/\\/g, "\\\\")}]`);
1575
+ return "";
1576
+ }).replace(/[.+^${}()|\\[\]]/g, "\\$&").replace(/\*\*\/?/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\0/g, "(?:.*/)?").replace(//g, () => classes.shift() ?? "");
1577
+ return new RegExp(`^(?:\\./)?${src}/?$`);
1578
+ }
1579
+ function expandGlob(t, listing) {
1580
+ const alts = expandBraces(t);
1581
+ if (!alts.some((a) => GLOB_CHARS.test(a))) return alts;
1582
+ const out3 = /* @__PURE__ */ new Set();
1583
+ for (const alt of alts) {
1584
+ if (!GLOB_CHARS.test(alt)) {
1585
+ out3.add(alt);
1586
+ continue;
1587
+ }
1588
+ let re;
1589
+ try {
1590
+ re = globToRegExp(alt);
1591
+ } catch {
1592
+ out3.add(alt);
1593
+ continue;
1594
+ }
1595
+ const depth = alt.replace(/^\.\//, "").replace(/\/+$/, "").split("/").length;
1596
+ for (const f of listing) {
1597
+ if (re.test(f)) {
1598
+ out3.add(f);
1599
+ continue;
1600
+ }
1601
+ const parts = f.split("/");
1602
+ if (parts.length > depth && !alt.includes("**")) {
1603
+ const dir = parts.slice(0, depth).join("/");
1604
+ if (re.test(dir)) out3.add(dir);
1605
+ }
1606
+ }
1607
+ }
1608
+ return [...out3];
1609
+ }
1610
+ function isGlob(t) {
1611
+ return GLOB_CHARS.test(t) || /\{[^{}]*\}/.test(t);
1612
+ }
1613
+ var GIT_OPT_WITH_ARG, GLOB_CHARS;
1540
1614
  var init_command = __esm({
1541
1615
  "src/detectors/command.ts"() {
1542
1616
  "use strict";
1543
1617
  GIT_OPT_WITH_ARG = /* @__PURE__ */ new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
1618
+ GLOB_CHARS = /[*?[]/;
1544
1619
  }
1545
1620
  });
1546
1621
 
@@ -2850,16 +2925,20 @@ function shellWritesHook(seg, toks, policy, ctx) {
2850
2925
  return null;
2851
2926
  }
2852
2927
  }
2853
- function xargsWritesHook(toks, fed, policy, ctx) {
2928
+ function xargsCommand(toks) {
2854
2929
  const at = toks.findIndex((t) => t === "xargs" || t.endsWith("/xargs"));
2855
2930
  if (at === -1) return null;
2931
+ let i = at + 1;
2932
+ while (i < toks.length && toks[i].startsWith("-")) i += /^-(?:I|n|L|P|d|a|s)$/.test(toks[i]) ? 2 : 1;
2933
+ return { cmd: (toks[i] ?? "").replace(/^.*\//, ""), args: toks.slice(i + 1), at: i };
2934
+ }
2935
+ function xargsWritesHook(toks, fed, policy, ctx) {
2936
+ const run = xargsCommand(toks);
2937
+ if (run === null) return null;
2856
2938
  const { hookish } = hookTests(policy, ctx);
2857
2939
  const fedHook = fed.some(hookish);
2858
2940
  if (!fedHook) return null;
2859
- let i = at + 1;
2860
- while (i < toks.length && toks[i].startsWith("-")) i += /^-(?:I|n|L|P|d|a|s)$/.test(toks[i]) ? 2 : 1;
2861
- const cmd = (toks[i] ?? "").replace(/^.*\//, "");
2862
- const args = toks.slice(i + 1);
2941
+ const { cmd, args, at: i } = run;
2863
2942
  if (["rm", "unlink", "shred", "truncate", "mv", "tee", "sponge"].includes(cmd)) return `xargs ${cmd} rewrites or removes a protected hook named earlier in the line`;
2864
2943
  if (cmd === "chmod" && chmodDropsExec(toks.slice(i))) return "xargs chmod removes execute permission from a hook named earlier in the line";
2865
2944
  if ((cmd === "sed" || cmd === "perl") && args.some((a) => /^-[A-Za-z]*i|^--in-place/.test(a))) return `xargs ${cmd} -i rewrites a protected hook named earlier in the line`;
@@ -8547,6 +8626,7 @@ var init_test_deletion = __esm({
8547
8626
  init_policy();
8548
8627
  init_finding();
8549
8628
  init_command();
8629
+ init_hook_wiring();
8550
8630
  init_files();
8551
8631
  init_repo();
8552
8632
  init_suite_config();
@@ -8683,16 +8763,31 @@ var init_test_deletion = __esm({
8683
8763
  }
8684
8764
  } else {
8685
8765
  let root = "";
8766
+ const fed = [];
8686
8767
  for (const seg of segments(c.raw)) {
8687
- const toks = tokens(seg).map(unquote);
8768
+ const toks = tokens(seg).map(shellWord);
8688
8769
  const cdTo = toks[0] === "cd" ? toks[1] : null;
8689
8770
  if (cdTo !== null) {
8690
8771
  root = cdTo === void 0 || /^[/~$]/.test(cdTo) || root === null ? null : joinRoot(root, cdTo);
8772
+ fed.push(...toks);
8691
8773
  continue;
8692
8774
  }
8693
8775
  const inRepo = (t) => root ? `${root}/${t}` : t;
8694
8776
  const listing = trackedFiles(ctx);
8695
- const testToks = toks.filter((t) => {
8777
+ const whole = (t) => /^(?:\.|\.\/|\*|\.\/\*)$/.test(t);
8778
+ const expand = (raw) => {
8779
+ const t = raw.replace(/^(?:of|if)=/, "");
8780
+ if (!isGlob(t) || whole(t)) return [t];
8781
+ if (listing && root !== null) return expandGlob(inRepo(t), listing).map((p) => root ? p.slice(root.length + 1) : p);
8782
+ return expandBraces(t).flatMap((alt) => {
8783
+ if (!isGlob(alt)) return [alt];
8784
+ const base = alt.split("/").pop() ?? "";
8785
+ if (/test|spec/i.test(base.replace(/[*?]|\[[^\]]*\]/g, ""))) return [alt.replace(/\*\*\//g, "").replace(/\*/g, "a").replace(/\?/g, "t")];
8786
+ const dir = alt.includes("/") ? alt.replace(/\/[^/]*$/, "") : "";
8787
+ return dir && !isGlob(dir) && containsProtected(dir, policy, "tests", ctx, "snapshots") ? [dir] : [];
8788
+ });
8789
+ };
8790
+ const specPath = (t) => {
8696
8791
  const resolved = root !== null && /(?:^|\/)\.{1,2}(?:\/|$)/.test(t) ? joinRoot(root, t) : null;
8697
8792
  const probe = resolved ?? t;
8698
8793
  if (!isSpec(probe)) return false;
@@ -8700,56 +8795,82 @@ var init_test_deletion = __esm({
8700
8795
  const looksLikeFile = /\.[A-Za-z0-9]+$/.test(probe.split("/").pop() ?? "");
8701
8796
  if (looksLikeFile || listing.includes(resolved ?? inRepo(t))) return true;
8702
8797
  return containsProtected(resolved ?? inRepo(t), policy, "tests", ctx, "snapshots");
8703
- });
8798
+ };
8704
8799
  const rootHasProtected = () => listing ? listing.some(isSpec) : true;
8705
- const dirToks = toks.filter((t) => {
8706
- if (t.startsWith("-") || testToks.includes(t)) return false;
8707
- const whole = /^(?:\.|\.\/|\*|\.\/\*)$/.test(t);
8708
- if (!whole && /[*?{}[\]]/.test(t)) return false;
8709
- if (root === null) return whole ? false : containsProtected(t, policy, "tests", ctx, "snapshots");
8710
- const dir = whole ? root : joinRoot(root, t);
8800
+ const dirPath = (t) => {
8801
+ if (t.startsWith("-")) return false;
8802
+ const isWhole = whole(t);
8803
+ if (!isWhole && isGlob(t)) return false;
8804
+ if (root === null) return isWhole ? false : containsProtected(t, policy, "tests", ctx, "snapshots");
8805
+ const dir = isWhole ? root : joinRoot(root, t);
8711
8806
  if (dir === null) return false;
8712
8807
  if (dir === "") return rootHasProtected();
8713
8808
  return containsProtected(dir, policy, "tests", ctx, "snapshots");
8714
- });
8809
+ };
8810
+ const xargs = xargsCommand(toks);
8811
+ const source = xargs ? [...fed, ...toks] : toks;
8812
+ fed.push(...toks);
8813
+ const testToks = source.filter((t) => expand(t).some(specPath));
8814
+ const dirToks = source.filter((t) => !t.startsWith("-") && !testToks.includes(t) && expand(t).some(dirPath));
8715
8815
  if (testToks.length === 0 && dirToks.length === 0) continue;
8716
8816
  const named = [...testToks, ...dirToks];
8717
- const cmd = toks[0] ?? "";
8817
+ let at = 0;
8818
+ while (at < toks.length && WRAPPERS.test(toks[at])) at++;
8819
+ const cmdToks = toks.slice(at);
8820
+ const cmd = xargs ? xargs.cmd : (cmdToks[0] ?? "").replace(/^.*\//, "");
8821
+ const args = xargs ? xargs.args : cmdToks.slice(1);
8822
+ const inPlace = (a) => /^-[A-Za-z]*i|^--in-place/.test(a);
8823
+ const isDir = (p) => listing ? listing.some((f) => f.startsWith(inRepo(p).replace(/\/+$/, "") + "/")) : /\/$/.test(p);
8824
+ const exists = (p) => !listing || root === null || listing.includes(inRepo(p.replace(/^\.\//, "")));
8825
+ const ARG_TAKING = /^-(?:t|m|o|g|S|e)$|^--(?:target-directory|mode|owner|group|suffix|rsh|exclude|include|files-from)$/;
8826
+ const positional = args.filter((a, i) => a !== "--" && !a.startsWith("-") && !ARG_TAKING.test(args[i - 1] ?? ""));
8718
8827
  let why = null;
8719
8828
  let evidence = seg;
8720
- const redirectTarget = seg.match(/>\s*(\S+)/)?.[1];
8721
- const redirectsOntoTest = redirectTarget ? isSpec(unquote(redirectTarget)) : false;
8829
+ const redirectsOntoTest = [...seg.matchAll(REDIRECT_TARGET)].some((m) => isSpec(shellWord(m[1])));
8722
8830
  const findDeletes = cmd === "find" && /\s(?:-delete\b|-exec\s+(?:rm|unlink)\b|-execdir\s+(?:rm|unlink)\b)/.test(seg);
8723
- const gitRestoresOld = cmd === "git" && /^git\s+(?:checkout|restore)\b/.test(seg) && (() => {
8831
+ const gitSub = cmd === "git" ? gitSubcommand(cmdToks) : null;
8832
+ const gitRestoresOld = (gitSub === "checkout" || gitSub === "restore") && (() => {
8724
8833
  const src = seg.match(/--source[= ]\s*(\S+)/)?.[1];
8725
- const revIdx = toks.findIndex((t, i) => i >= 2 && !t.startsWith("-") && !named.includes(t) && t !== "--");
8726
- const rev2 = src ?? (revIdx >= 0 ? toks[revIdx] : void 0);
8834
+ const revIdx = cmdToks.findIndex((t, i) => i >= 2 && !t.startsWith("-") && !named.includes(t) && t !== "--");
8835
+ const rev2 = src ?? (revIdx >= 0 ? cmdToks[revIdx] : void 0);
8727
8836
  if (!rev2 || rev2 === "HEAD" || rev2 === "@" || revIsHead(rev2, ctx) === true) return false;
8728
8837
  if (src || seg.includes(" -- ")) return true;
8729
- return revIdx >= 0 && toks.some((t, i) => i > revIdx && named.includes(t));
8838
+ return revIdx >= 0 && cmdToks.some((t, i) => i > revIdx && named.includes(t));
8730
8839
  })();
8731
- if (/\brm\b/.test(seg) && !/^(?:git|npm|yarn|pnpm|bun)\b/.test(seg)) {
8732
- why = "rm deletes a test file";
8733
- } else if (cmd === "git" && /^git\s+rm\b/.test(seg)) {
8840
+ const dests = cmd === "cp" || cmd === "install" || cmd === "rsync" || cmd === "ln" || cmd === "mv" ? destinations(cmd, args, isDir) : [];
8841
+ const overwrites = dests.some((d) => isSpec(d) && exists(d)) && positional.some((p) => !dests.includes(p) && !isSpec(p));
8842
+ if (xargs) {
8843
+ if (cmd === "rm" || cmd === "unlink" || cmd === "shred") why = `xargs ${cmd} deletes a test file named earlier in the line`;
8844
+ else if (cmd === "truncate") why = "xargs truncate empties a test file named earlier in the line";
8845
+ else if (cmd === "mv") why = "xargs mv moves a test file named earlier in the line";
8846
+ else if (cmd === "tee" || cmd === "sponge") why = `xargs ${cmd} rewrites a test file named earlier in the line`;
8847
+ else if ((cmd === "sed" || cmd === "perl") && args.some(inPlace)) why = `xargs ${cmd} -i rewrites a test file named earlier in the line`;
8848
+ } else if (cmd === "rm" || cmd === "unlink" || cmd === "shred") {
8849
+ why = `${cmd} deletes a test file`;
8850
+ } else if (gitSub === "rm") {
8734
8851
  why = "git rm deletes a test file";
8735
8852
  } else if (cmd === "sed" && SED_INPLACE.test(seg)) {
8736
8853
  why = "sed -i rewrites a test file in place";
8737
8854
  } else if (cmd === "perl" && /(?:^|\s)-[a-zA-Z]*i\b|\s-i\S*/.test(seg)) {
8738
8855
  why = "perl -i rewrites a test file in place";
8739
- } else if (/\btruncate\b/.test(seg)) {
8856
+ } else if (cmd === "truncate") {
8740
8857
  why = "truncate empties a test file";
8741
8858
  } else if (redirectsOntoTest) {
8742
8859
  why = "a redirect overwrites/empties a test file";
8743
- } else if (cmd === "cp" && testToks.length > 0 && isSpec(toks[toks.length - 1] ?? "") && !isSpec(toks[toks.length - 2] ?? "")) {
8744
- why = "cp overwrites a test file with something else";
8745
- } else if (cmd === "tee" && !toks.includes("-a") && !toks.includes("--append")) {
8860
+ } else if (cmd !== "mv" && overwrites) {
8861
+ why = `${cmd} overwrites a test file with something else`;
8862
+ } else if (cmd === "tee" && !args.includes("-a") && !args.includes("--append")) {
8746
8863
  why = "tee overwrites a test file";
8864
+ } else if (cmd === "sponge" && !args.includes("-a")) {
8865
+ why = "sponge rewrites a test file in place";
8866
+ } else if (cmd === "dd" && args.some((a) => a.startsWith("of=") && isSpec(a.slice(3)))) {
8867
+ why = "dd rewrites a test file";
8747
8868
  } else if (findDeletes) {
8748
8869
  why = "find -delete removes test files";
8749
8870
  } else if (gitRestoresOld) {
8750
8871
  why = "git checkout <rev> replaces a test file with an older version";
8751
- } else if (/\bmv\b/.test(seg)) {
8752
- const dest = toks[toks.length - 1];
8872
+ } else if (cmd === "mv") {
8873
+ const dest = positional[positional.length - 1];
8753
8874
  if (dest && testToks.length > 0 && !isSpec(dest)) {
8754
8875
  why = "mv renames a test file out of the test glob";
8755
8876
  evidence = `${testToks[0]} \u2192 ${dest}`;
@@ -8759,6 +8880,8 @@ var init_test_deletion = __esm({
8759
8880
  } else if (dest && testToks.length === 0 && dirToks[0] !== dest && leavesGlob(dirToks[0] ?? "", dest, policy, ctx)) {
8760
8881
  why = "mv renames a test directory so its specs leave the test glob";
8761
8882
  evidence = `${dirToks[0]} \u2192 ${dest}`;
8883
+ } else if (overwrites) {
8884
+ why = "mv overwrites a test file with something else";
8762
8885
  }
8763
8886
  }
8764
8887
  if (why) {
@@ -16807,7 +16930,8 @@ function writeVerifyCommand(cwd, command) {
16807
16930
  async function runOnboard(opts, io = {}) {
16808
16931
  const requestedCwd = resolve11(opts.cwd ?? process.cwd());
16809
16932
  const out3 = io.out ?? ((line) => void process.stdout.write(line + "\n"));
16810
- const rawErr = (line) => void process.stderr.write(terminalText(line) + "\n");
16933
+ const errLine = (line) => void process.stderr.write(line + "\n");
16934
+ const rawErr = (line) => errLine(terminalText(line));
16811
16935
  const platform = io.platform ?? process.platform;
16812
16936
  const colour = io.colour ?? colourEnabled(process.env, process.stdout);
16813
16937
  const runners = {
@@ -16829,7 +16953,10 @@ async function runOnboard(opts, io = {}) {
16829
16953
  const status = (label, text, kind = "info") => {
16830
16954
  out3(paint2(label.padEnd(8), (kind === "bad" ? BOLD2 : "") + tone(kind), colour) + " " + terminalText(text));
16831
16955
  };
16832
- const fail = (text) => rawErr(paint2("ERROR ", BOLD2 + RED2, colour) + " " + text);
16956
+ const errStatus = (label, text, kind = "info") => {
16957
+ errLine(paint2(label.padEnd(8), (kind === "bad" ? BOLD2 : "") + tone(kind), colour) + " " + terminalText(text));
16958
+ };
16959
+ const fail = (text) => errStatus("ERROR", text, "bad");
16833
16960
  if (!interactive && !scripted) {
16834
16961
  fail("onboard needs an interactive terminal.");
16835
16962
  rawErr('Use `tamperward onboard --yes --verify-command "<suite command>"` for scripted setup,');
@@ -16848,11 +16975,16 @@ async function runOnboard(opts, io = {}) {
16848
16975
  } catch {
16849
16976
  }
16850
16977
  if (!sameRoot) {
16851
- fail("current directory is not the Git repository root.");
16852
- rawErr("Current: " + requestedCwd);
16853
- rawErr("Git root: " + cwd);
16854
- rawErr("TamperWard installs repository-wide hooks and CI, so it will not mix a child directory with its parent repository.");
16855
- rawErr("Run from the Git root, or run `git init` in the child directory if it should be a separate repository.");
16978
+ fail("Repository root mismatch.");
16979
+ rawErr("");
16980
+ errStatus("CURRENT", requestedCwd, "dim");
16981
+ errStatus("GIT ROOT", cwd, "info");
16982
+ rawErr("");
16983
+ rawErr("TamperWard installs repository-wide hooks and CI, so setup must run at the repository root.");
16984
+ rawErr("It will not attach child-directory setup to a parent repository.");
16985
+ rawErr("");
16986
+ errStatus("NEXT", "Run `tamperward onboard` from the Git root shown above.", "warn");
16987
+ errStatus("OR", "Run `git init` here first if the current directory should be a separate repository.", "dim");
16856
16988
  return 2;
16857
16989
  }
16858
16990
  const prompt = { asker: null };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.24.4",
3
+ "version": "2.24.6",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",