tamperward 2.23.1 → 2.23.3

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 +116 -36
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -9934,7 +9934,7 @@ var init_verifier_limits = __esm({
9934
9934
  // src/cli/init.ts
9935
9935
  import { execFileSync as execFileSync6 } from "node:child_process";
9936
9936
  import { createHash as createHash6 } from "node:crypto";
9937
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync2 } from "node:fs";
9937
+ import { existsSync as existsSync10, lstatSync as lstatSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7, chmodSync as chmodSync2, statSync as statSync2 } from "node:fs";
9938
9938
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute8, join as join14, relative as relative3, resolve as resolve3 } from "node:path";
9939
9939
  function gitConfig(cwd, key2) {
9940
9940
  try {
@@ -10112,6 +10112,65 @@ function planPolicy(cwd) {
10112
10112
  apply: () => writeFileSync7(path, POLICY_CONTENT)
10113
10113
  };
10114
10114
  }
10115
+ function planGitignore(cwd) {
10116
+ const rel = ".gitignore";
10117
+ const path = join14(cwd, rel);
10118
+ if (!existsSync10(join14(cwd, "node_modules"))) return null;
10119
+ let existing = "";
10120
+ if (existsSync10(path)) {
10121
+ let st;
10122
+ try {
10123
+ st = lstatSync7(path);
10124
+ } catch (e) {
10125
+ return { item: "gitignore", path: rel, status: "error", detail: "cannot inspect \u2014 " + errText3(e) };
10126
+ }
10127
+ if (!st.isFile()) {
10128
+ return {
10129
+ item: "gitignore",
10130
+ path: rel,
10131
+ status: "error",
10132
+ detail: st.isSymbolicLink() ? "is a symbolic link \u2014 refusing to follow it" : "exists but is not a regular file \u2014 refusing to write"
10133
+ };
10134
+ }
10135
+ existing = readFileSync12(path, "utf8");
10136
+ }
10137
+ try {
10138
+ const tracked = execFileSync6("git", ["ls-files", "--cached", "--", "node_modules"], {
10139
+ cwd,
10140
+ encoding: "utf8",
10141
+ stdio: ["ignore", "pipe", "pipe"]
10142
+ }).trim();
10143
+ if (tracked) {
10144
+ return {
10145
+ item: "gitignore",
10146
+ path: rel,
10147
+ status: "skip",
10148
+ detail: "node_modules is already tracked/staged \u2014 left unchanged; TamperWard will scan it as repository content"
10149
+ };
10150
+ }
10151
+ } catch {
10152
+ }
10153
+ const meaningful = existing.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
10154
+ if (meaningful.some((line) => line.startsWith("!") && line.includes("node_modules"))) {
10155
+ return {
10156
+ item: "gitignore",
10157
+ path: rel,
10158
+ status: "skip",
10159
+ detail: "contains an explicit node_modules negation \u2014 left untouched; review the repository ignore policy before committing dependencies"
10160
+ };
10161
+ }
10162
+ if (meaningful.some((line) => /^(?:\/|\*\*\/)?node_modules\/?$/.test(line))) {
10163
+ return { item: "gitignore", path: rel, status: "ok", detail: "node_modules is already ignored by the repository" };
10164
+ }
10165
+ const next = existing.length === 0 ? "node_modules/\n" : existing.replace(/\n?$/, "\n") + "node_modules/\n";
10166
+ return {
10167
+ item: "gitignore",
10168
+ path: rel,
10169
+ status: existsSync10(path) ? "update" : "create",
10170
+ detail: "exclude installed Node dependencies from repository diffs",
10171
+ apply: () => writeFileSync7(path, next)
10172
+ };
10173
+ }
10115
10174
  function planClaudeHooks(cwd) {
10116
10175
  const rel = ".claude/settings.json";
10117
10176
  const path = join14(cwd, rel);
@@ -10425,8 +10484,10 @@ function planned(item, path, plan) {
10425
10484
  }
10426
10485
  function planInit(cwd, opts = {}) {
10427
10486
  requireShippedVersion();
10487
+ const gitignore = planGitignore(cwd);
10428
10488
  return [
10429
10489
  planned("policy", POLICY_FILE, () => planPolicy(cwd)),
10490
+ ...gitignore ? [gitignore] : [],
10430
10491
  planned("agent", ".claude/settings.json", () => planClaudeHooks(cwd)),
10431
10492
  planned("pre-commit", "(hooks)", () => planPreCommit(cwd)),
10432
10493
  planned("ci", ".github/workflows/tamperward.yml", () => planWorkflow(cwd, opts.forceWorkflow ?? false)),
@@ -10660,7 +10721,7 @@ var init_fingerprint = __esm({
10660
10721
  import { createHash as createHash8 } from "node:crypto";
10661
10722
  import {
10662
10723
  existsSync as existsSync11,
10663
- lstatSync as lstatSync7,
10724
+ lstatSync as lstatSync8,
10664
10725
  readdirSync as readdirSync3,
10665
10726
  readFileSync as readFileSync13,
10666
10727
  readlinkSync as readlinkSync2,
@@ -10685,7 +10746,7 @@ function inside(root, candidate) {
10685
10746
  return rel === "" || rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute9(rel);
10686
10747
  }
10687
10748
  function modeKind(path) {
10688
- const st = lstatSync7(path);
10749
+ const st = lstatSync8(path);
10689
10750
  if (st.isFile()) return { kind: "file", mode: st.mode };
10690
10751
  if (st.isDirectory()) return { kind: "dir", mode: st.mode };
10691
10752
  if (st.isSymbolicLink()) return { kind: "symlink", mode: st.mode };
@@ -10695,7 +10756,7 @@ function fingerprintRoot(root, cwd, h) {
10695
10756
  let rootStat;
10696
10757
  let currentReal;
10697
10758
  try {
10698
- rootStat = lstatSync7(root.path);
10759
+ rootStat = lstatSync8(root.path);
10699
10760
  currentReal = realpathSync4(root.path);
10700
10761
  } catch (e) {
10701
10762
  throw new Unattestable(
@@ -11865,7 +11926,7 @@ var init_verifier_backend = __esm({
11865
11926
  // src/cli/verify.ts
11866
11927
  import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:child_process";
11867
11928
  import { createHash as createHash10 } from "node:crypto";
11868
- import { chmodSync as chmodSync3, cpSync, lstatSync as lstatSync8, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync5, symlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
11929
+ import { chmodSync as chmodSync3, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync5, symlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
11869
11930
  import { tmpdir as tmpdir4 } from "node:os";
11870
11931
  import { dirname as dirname7, isAbsolute as isAbsolute10, join as join18, relative as relative5, resolve as resolve6, sep as sep3 } from "node:path";
11871
11932
  function oracleAssuranceReport() {
@@ -11938,7 +11999,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
11938
11999
  const current = join18(base, ...stack);
11939
12000
  let st;
11940
12001
  try {
11941
- st = lstatSync8(current);
12002
+ st = lstatSync9(current);
11942
12003
  } catch {
11943
12004
  continue;
11944
12005
  }
@@ -11974,7 +12035,7 @@ function rejectLinkedParent(cwd, rel) {
11974
12035
  current = join18(current, part);
11975
12036
  let st;
11976
12037
  try {
11977
- st = lstatSync8(current);
12038
+ st = lstatSync9(current);
11978
12039
  } catch {
11979
12040
  continue;
11980
12041
  }
@@ -11991,7 +12052,7 @@ function materialize(cwd, dest, dependencyRoot) {
11991
12052
  rejectLinkedParent(cwd, rel);
11992
12053
  let st;
11993
12054
  try {
11994
- st = lstatSync8(src);
12055
+ st = lstatSync9(src);
11995
12056
  } catch {
11996
12057
  continue;
11997
12058
  }
@@ -12016,7 +12077,7 @@ function materialize(cwd, dest, dependencyRoot) {
12016
12077
  }
12017
12078
  function dropSymlink(p) {
12018
12079
  try {
12019
- if (lstatSync8(p).isSymbolicLink()) rmSync5(p, { force: true });
12080
+ if (lstatSync9(p).isSymbolicLink()) rmSync5(p, { force: true });
12020
12081
  } catch {
12021
12082
  }
12022
12083
  }
@@ -12058,7 +12119,7 @@ function overlayDigest(dest, paths) {
12058
12119
  h.update("\0");
12059
12120
  const p = join18(dest, rel);
12060
12121
  try {
12061
- const st = lstatSync8(p);
12122
+ const st = lstatSync9(p);
12062
12123
  h.update(String(st.mode));
12063
12124
  h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
12064
12125
  } catch {
@@ -14619,7 +14680,7 @@ var init_trace_verify = __esm({
14619
14680
  // src/cli/onboard.ts
14620
14681
  import { execFileSync as execFileSync12 } from "node:child_process";
14621
14682
  import { randomBytes as randomBytes2 } from "node:crypto";
14622
- import { lstatSync as lstatSync9, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
14683
+ import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync2, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
14623
14684
  import { tmpdir as tmpdir7 } from "node:os";
14624
14685
  import { join as join22, relative as relative7, resolve as resolve10, sep as sep5 } from "node:path";
14625
14686
  import { createInterface } from "node:readline";
@@ -14699,7 +14760,7 @@ function atomicReplaceFile(path, content, mode) {
14699
14760
  }
14700
14761
  function writeVerifyCommand(cwd, command) {
14701
14762
  const path = join22(cwd, POLICY_FILE);
14702
- const st = lstatSync9(path, { throwIfNoEntry: false });
14763
+ const st = lstatSync10(path, { throwIfNoEntry: false });
14703
14764
  if (st && (!st.isFile() || st.isSymbolicLink())) {
14704
14765
  return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
14705
14766
  }
@@ -14818,26 +14879,35 @@ async function runOnboard(opts, io = {}) {
14818
14879
  status("LIMITED", platformLabel(platform) + ": check works here; local verify and `run` are unavailable. Use the container verifier for final verification.", "warn");
14819
14880
  }
14820
14881
  const head = git5(cwd, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])?.trim() ?? null;
14821
- if (!head) status("ACTION", "No commit yet. Commit once before pristine verification or the safe demo.", "warn");
14822
14882
  const treeStatus = git5(cwd, ["status", "--porcelain", "--untracked-files=all"]) ?? "";
14823
14883
  const dirty = treeStatus.split("\n").filter(Boolean).map((line) => line.slice(3).split(" -> ").at(-1) ?? "");
14824
- const owned = new Set(planInit(cwd).map((a) => a.path));
14825
- const foreign = dirty.filter((p) => !owned.has(p));
14826
- if (dirty.length === 0) {
14827
- status("OK", "Working tree is clean.", "ok");
14828
- } else if (foreign.length === 0) {
14829
- status("ACTION", dirty.length + " uncommitted setup file(s) from TamperWard; commit them when setup is complete.", "warn");
14884
+ if (!head) {
14885
+ const count = dirty.length;
14886
+ status(
14887
+ "ACTION",
14888
+ count ? "No first commit yet; " + count + " path(s) are not part of a committed baseline. Create the initial commit before pristine verification or the safe demo." : "No first commit yet. Create the initial commit before pristine verification or the safe demo.",
14889
+ "warn"
14890
+ );
14830
14891
  } else {
14831
- status("ACTION", dirty.length + " existing changed/untracked path(s). TamperWard will not stash or reset them.", "warn");
14832
- if (!await confirm("Continue with the existing working-tree changes?", false, true)) {
14833
- status("STOPPED", "Commit or stash your work, then re-run `tamperward onboard`.", "warn");
14834
- return 2;
14892
+ const owned = new Set(planInit(cwd).map((a) => a.path));
14893
+ const foreign = dirty.filter((p) => !owned.has(p));
14894
+ if (dirty.length === 0) {
14895
+ status("OK", "Working tree is clean.", "ok");
14896
+ } else if (foreign.length === 0) {
14897
+ status("ACTION", dirty.length + " uncommitted setup file(s) from TamperWard; commit them when setup is complete.", "warn");
14898
+ } else {
14899
+ status("ACTION", dirty.length + " existing changed/untracked path(s). TamperWard will not stash or reset them.", "warn");
14900
+ if (!await confirm("Continue with the existing working-tree changes?", false, true)) {
14901
+ status("STOPPED", "Commit or stash your work, then re-run `tamperward onboard`.", "warn");
14902
+ return 2;
14903
+ }
14835
14904
  }
14836
14905
  }
14837
14906
  section(2);
14838
14907
  const plan = planInit(cwd);
14839
14908
  const names = {
14840
14909
  policy: "Policy",
14910
+ gitignore: "Git ignore",
14841
14911
  agent: "Claude hooks",
14842
14912
  "pre-commit": "Pre-commit",
14843
14913
  ci: "CI workflow",
@@ -14950,11 +15020,11 @@ async function runOnboard(opts, io = {}) {
14950
15020
  status("SKIP", "Verification skipped. Run `tamperward verify` when ready.", "dim");
14951
15021
  }
14952
15022
  if (!opts.skipDemo) {
14953
- const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the optional safe tamper demo? (temporary worktree only)", false, false);
14954
- if (wanted && head) {
14955
- runDemo(cwd, head, runners, (line) => out3(terminalText(line)));
14956
- } else if (wanted) {
14957
- status("SKIP", "Demo needs at least one commit.", "dim");
15023
+ if (!head) {
15024
+ status("SKIP", "Safe demo becomes available after the first commit.", "dim");
15025
+ } else {
15026
+ const wanted = scripted ? Boolean(opts.demo) : opts.demo ? true : await confirm("Run the optional safe tamper demo? (temporary worktree only)", false, false);
15027
+ if (wanted) runDemo(cwd, head, runners, (line) => out3(terminalText(line)));
14958
15028
  }
14959
15029
  }
14960
15030
  section(4);
@@ -14982,8 +15052,10 @@ async function runOnboard(opts, io = {}) {
14982
15052
  if (check2.state === "OK" || check2.id === "observer") continue;
14983
15053
  seenFailure.add(check2.id);
14984
15054
  if (check2.id === "platform" && platform !== "linux") {
14985
- const detail = localVerifySupported ? platformLabel(platform) + ": `tamperward run` requires Linux; check + verify remain available." : platformLabel(platform) + ": `tamperward run` and local verify are unavailable; check remains available.";
14986
- status("LIMITED", detail, "warn");
15055
+ continue;
15056
+ }
15057
+ if (check2.id === "verifier" && !verifyCommand && check2.state === "BROKEN") {
15058
+ status("ACTION", "Verification is not configured; choose the trusted test command.", "warn");
14987
15059
  } else {
14988
15060
  renderCheck(check2, out3, colour);
14989
15061
  }
@@ -14998,15 +15070,24 @@ async function runOnboard(opts, io = {}) {
14998
15070
  else status("BLOCKED", "Fix the broken item(s) above before relying on the gate.", "bad");
14999
15071
  if (doctorOutcome.github) {
15000
15072
  status("OK", "GitHub authority enforced for " + doctorOutcome.github.repo + "#" + doctorOutcome.github.branch + ".", "ok");
15073
+ } else if (!repo) {
15074
+ status("ACTION", "GitHub authority is not configured for this repository.", "warn");
15075
+ out3(paint2(" If GitHub will provide repository authority, run: " + terminalText(doctorCommand), DIM2, colour));
15076
+ out3(paint2(" If this repository will use GitHub CI authority, configure:", DIM2, colour));
15077
+ for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
15001
15078
  } else {
15002
15079
  status("ACTION", "GitHub authority is not verified yet.", "warn");
15003
15080
  out3(paint2(" " + terminalText(doctorCommand), DIM2, colour));
15004
- for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
15081
+ out3(paint2(" GitHub authority requires all three controls:", DIM2, colour));
15082
+ for (const line of MANUAL_CONTROLS) out3(paint2(" " + line, DIM2, colour));
15005
15083
  }
15006
15084
  const commitPaths = [...new Set(wrote)].filter((path) => committableSetupPath(cwd, path));
15007
15085
  if (commitPaths.length) {
15008
15086
  status("NEXT", "Commit repository setup files: " + commitPaths.join(", "), "info");
15009
15087
  }
15088
+ if (!head) {
15089
+ status("NEXT", "Create the initial repository commit before verify or the safe demo.", "info");
15090
+ }
15010
15091
  if (!verifyCommand) {
15011
15092
  status("NEXT", "Choose the test command TamperWard should trust for verification.", "info");
15012
15093
  }
@@ -15071,7 +15152,7 @@ function runDemo(cwd, head, runners, out3) {
15071
15152
  return;
15072
15153
  }
15073
15154
  const targetPath = join22(wt, target.path);
15074
- const targetStat = lstatSync9(targetPath, { throwIfNoEntry: false });
15155
+ const targetStat = lstatSync10(targetPath, { throwIfNoEntry: false });
15075
15156
  if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
15076
15157
  out3("Demo skipped: the selected test is not a regular file in the disposable worktree.");
15077
15158
  return;
@@ -15136,10 +15217,9 @@ var init_onboard = __esm({
15136
15217
  "Summary"
15137
15218
  ];
15138
15219
  MANUAL_CONTROLS = [
15139
- "GitHub still needs three repository controls:",
15140
- " required check: tamperward",
15141
- " Code Owner review",
15142
- " dismiss stale approvals when new commits are pushed"
15220
+ "required check: tamperward",
15221
+ "Code Owner review",
15222
+ "dismiss stale approvals when new commits are pushed"
15143
15223
  ];
15144
15224
  ESC2 = "\x1B";
15145
15225
  RESET2 = `${ESC2}[0m`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.1",
3
+ "version": "2.23.3",
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",