diffprism 0.45.0 → 0.47.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/README.md CHANGED
@@ -102,6 +102,41 @@ diffprism review main..feature # Branch diff
102
102
 
103
103
  Running multiple Claude Code sessions? All reviews appear in one browser dashboard with status badges, branch info, and desktop notifications.
104
104
 
105
+ ## Commit Gate
106
+
107
+ Review is only reliable if something other than memory triggers it. `diffprism hook`
108
+ wires the review into your pre-commit hook, so a substantial change opens a review
109
+ before it can land.
110
+
111
+ ```bash
112
+ diffprism hook install # Add the gate to this repo's pre-commit hook
113
+ diffprism hook uninstall # Remove it
114
+ ```
115
+
116
+ By default a staged diff of **120+ changed lines** opens a review; anything smaller
117
+ commits untouched. A gate that stops every commit is one you learn to skip with
118
+ `--no-verify`, and a skipped gate is worse than none. Tune it per repo:
119
+
120
+ ```bash
121
+ git config diffprism.gate-lines 200
122
+ ```
123
+
124
+ Approve and the commit proceeds. Request changes and the commit is blocked — with
125
+ your comments printed in the output, so the agent that ran `git commit` can read
126
+ what you asked for and fix it without another round trip:
127
+
128
+ ```
129
+ 140 staged lines (gate at 120) — opening DiffPrism review...
130
+
131
+ feature.ts:12 [must_fix] these should be a single exported record
132
+ feature.ts:88 [question] is this range meant to be inclusive?
133
+
134
+ Commit blocked: the review requested changes.
135
+ ```
136
+
137
+ Install adds one line between markers, so uninstall removes exactly that and leaves
138
+ the rest of your hook alone. Repos using `core.hooksPath` are handled.
139
+
105
140
  ## Features
106
141
 
107
142
  - **AI-powered PR review** — Your AI gets full codebase context via 14 MCP tools
@@ -124,6 +159,8 @@ diffprism setup --global # Global setup (no git repo needed)
124
159
  diffprism server # Start the background server
125
160
  diffprism server status # Check server status
126
161
  diffprism server stop # Stop the server
162
+ diffprism hook install # Gate commits on a review
163
+ diffprism hook uninstall # Remove the gate
127
164
  diffprism teardown # Remove configuration
128
165
  ```
129
166
 
package/dist/bin.js CHANGED
@@ -5,15 +5,17 @@ import {
5
5
  } from "./chunk-24B33UN6.js";
6
6
  import {
7
7
  demo
8
- } from "./chunk-RVI74JF5.js";
8
+ } from "./chunk-TZ5SGWHP.js";
9
9
  import {
10
10
  ensureServer,
11
11
  isServerAlive,
12
12
  readServerFile,
13
13
  startGlobalServer,
14
14
  submitReviewToServer
15
- } from "./chunk-RICVPPTE.js";
16
- import "./chunk-QGWYCEJN.js";
15
+ } from "./chunk-PQWU5NW4.js";
16
+ import {
17
+ getDiff
18
+ } from "./chunk-QGWYCEJN.js";
17
19
  import "./chunk-DHCVZGHE.js";
18
20
  import "./chunk-JSBRDJBE.js";
19
21
 
@@ -39,21 +41,26 @@ async function review(ref, flags) {
39
41
  await reviewLocalFlow(diffRef, flags);
40
42
  }
41
43
  } catch (err) {
42
- const message = err instanceof Error ? err.message : String(err);
43
- console.error(`Error: ${message}`);
44
+ const message2 = err instanceof Error ? err.message : String(err);
45
+ console.error(`Error: ${message2}`);
44
46
  process.exit(1);
45
47
  }
46
48
  }
47
49
  async function reviewLocalFlow(diffRef, flags) {
48
50
  const serverInfo = await ensureServer({ dev: flags.dev });
49
- console.log("Opening review in browser...");
51
+ console.error("Opening review in browser...");
50
52
  const { result } = await submitReviewToServer(serverInfo, diffRef, {
51
53
  title: flags.title,
52
54
  cwd: process.cwd(),
53
55
  diffRef
54
56
  });
55
57
  console.log(JSON.stringify(result, null, 2));
56
- process.exit(0);
58
+ if (!result) {
59
+ console.error("No review result was returned.");
60
+ process.exit(1);
61
+ }
62
+ const approved = result.decision === "approved" || result.decision === "approved_with_comments";
63
+ process.exit(approved ? 0 : 1);
57
64
  }
58
65
  async function reviewPrFlow(pr, flags) {
59
66
  const { owner, repo, number } = parsePrRef(pr);
@@ -370,7 +377,7 @@ async function setupInteractive(flags) {
370
377
  }
371
378
  async function runDemo(dev) {
372
379
  console.log("");
373
- const { demo: demo2 } = await import("./demo-WVNNI6CE.js");
380
+ const { demo: demo2 } = await import("./demo-H2NAMKCT.js");
374
381
  await demo2({ dev });
375
382
  }
376
383
  async function setupBatch(flags) {
@@ -703,9 +710,9 @@ async function server(flags) {
703
710
  await new Promise(() => {
704
711
  });
705
712
  } catch (err) {
706
- const message = err instanceof Error ? err.message : String(err);
713
+ const message2 = err instanceof Error ? err.message : String(err);
707
714
  if (!isDaemon) {
708
- console.error(`Error starting server: ${message}`);
715
+ console.error(`Error starting server: ${message2}`);
709
716
  }
710
717
  process.exit(1);
711
718
  }
@@ -778,8 +785,8 @@ async function serverStatus() {
778
785
  }
779
786
  }
780
787
  } catch (err) {
781
- const message = err instanceof Error ? err.message : String(err);
782
- console.error(`Error checking server status: ${message}`);
788
+ const message2 = err instanceof Error ? err.message : String(err);
789
+ console.error(`Error checking server status: ${message2}`);
783
790
  process.exit(1);
784
791
  }
785
792
  }
@@ -813,7 +820,7 @@ async function fetchStatus(httpPort) {
813
820
  return await response.json();
814
821
  }
815
822
  function printStatus(status, httpPort) {
816
- const version = true ? "0.45.0" : "0.0.0-dev";
823
+ const version = true ? "0.47.0" : "0.0.0-dev";
817
824
  console.log(`
818
825
  DiffPrism v${version}
819
826
  `);
@@ -845,19 +852,211 @@ async function defaultAction() {
845
852
  await open(status.uiUrl);
846
853
  }
847
854
  } catch (err) {
848
- const message = err instanceof Error ? err.message : String(err);
849
- console.error(`Error fetching server status: ${message}`);
855
+ const message2 = err instanceof Error ? err.message : String(err);
856
+ console.error(`Error fetching server status: ${message2}`);
850
857
  process.exit(1);
851
858
  }
852
859
  }
853
860
 
861
+ // cli/src/commands/hook.ts
862
+ import { execFileSync } from "child_process";
863
+ import fs4 from "fs";
864
+ import path4 from "path";
865
+ var DEFAULT_MIN_LINES = 120;
866
+ var MARKER_START = "# >>> diffprism >>>";
867
+ var MARKER_END = "# <<< diffprism <<<";
868
+ var HOOK_LINE = "diffprism hook pre-commit || exit 1";
869
+ async function preCommitHook(flags = {}) {
870
+ const cwd = process.cwd();
871
+ const minLines = resolveMinLines(flags, cwd);
872
+ let changedLines;
873
+ try {
874
+ const { diffSet } = getDiff("staged", { cwd });
875
+ changedLines = diffSet.files.reduce(
876
+ (total, file) => total + file.additions + file.deletions,
877
+ 0
878
+ );
879
+ } catch (err) {
880
+ fail(`Could not read the staged diff: ${message(err)}`);
881
+ return;
882
+ }
883
+ if (changedLines === 0) {
884
+ process.exit(0);
885
+ }
886
+ if (changedLines < minLines) {
887
+ process.exit(0);
888
+ }
889
+ console.error(
890
+ `${changedLines} staged lines (gate at ${minLines}) \u2014 opening DiffPrism review...`
891
+ );
892
+ let review2 = null;
893
+ try {
894
+ const serverInfo = await ensureServer({ dev: flags.dev });
895
+ const { result } = await submitReviewToServer(serverInfo, "staged", {
896
+ cwd,
897
+ diffRef: "staged",
898
+ title: "Pre-commit review"
899
+ });
900
+ review2 = result;
901
+ } catch (err) {
902
+ fail(`DiffPrism could not run the review: ${message(err)}`);
903
+ return;
904
+ }
905
+ const decision = review2?.decision;
906
+ if (decision === "approved" || decision === "approved_with_comments") {
907
+ console.error(`Review: ${decision} \u2014 proceeding.`);
908
+ process.exit(0);
909
+ }
910
+ if (decision === "changes_requested") {
911
+ printComments(review2?.comments ?? []);
912
+ fail("Commit blocked: the review requested changes.");
913
+ return;
914
+ }
915
+ if (decision === "dismissed") {
916
+ fail("Commit blocked: the review was dismissed without a decision.");
917
+ return;
918
+ }
919
+ fail(
920
+ `Commit blocked: no review decision was returned (got ${String(decision)}).`
921
+ );
922
+ }
923
+ function printComments(comments) {
924
+ if (comments.length === 0) {
925
+ return;
926
+ }
927
+ console.error("");
928
+ for (const c of comments) {
929
+ console.error(` ${c.file}:${c.line} [${c.type}] ${c.body}`);
930
+ }
931
+ console.error("");
932
+ }
933
+ function installHook() {
934
+ const hookPath = resolveHookPath(process.cwd());
935
+ const existing = fs4.existsSync(hookPath) ? fs4.readFileSync(hookPath, "utf8") : "";
936
+ if (existing.includes(MARKER_START)) {
937
+ console.log(`Already installed in ${hookPath}`);
938
+ return;
939
+ }
940
+ const block = `${MARKER_START}
941
+ ${HOOK_LINE}
942
+ ${MARKER_END}
943
+ `;
944
+ const base = existing === "" ? "#!/bin/sh\n" : ensureTrailingNewline(existing);
945
+ const created = existing === "";
946
+ fs4.mkdirSync(path4.dirname(hookPath), { recursive: true });
947
+ fs4.writeFileSync(hookPath, `${base}
948
+ ${block}`);
949
+ fs4.chmodSync(hookPath, 493);
950
+ console.log(`${created ? "Created" : "Updated"} ${hookPath}`);
951
+ console.log(
952
+ `Staged changes of ${DEFAULT_MIN_LINES}+ lines now open a review before the commit lands.`
953
+ );
954
+ console.log("Tune with: git config diffprism.gate-lines <n>");
955
+ console.log("Remove with: diffprism hook uninstall");
956
+ }
957
+ function uninstallHook() {
958
+ const hookPath = resolveHookPath(process.cwd());
959
+ if (!fs4.existsSync(hookPath)) {
960
+ console.log("Nothing to remove \u2014 no hook file.");
961
+ return;
962
+ }
963
+ const existing = fs4.readFileSync(hookPath, "utf8");
964
+ if (!existing.includes(MARKER_START)) {
965
+ console.log(`Nothing to remove \u2014 no diffprism block in ${hookPath}`);
966
+ return;
967
+ }
968
+ const cleaned = removeMarkedBlock(existing);
969
+ if (/^\s*(#![^\n]*)?\s*$/.test(cleaned)) {
970
+ fs4.rmSync(hookPath);
971
+ console.log(`Removed ${hookPath} (it contained nothing else).`);
972
+ return;
973
+ }
974
+ fs4.writeFileSync(hookPath, cleaned);
975
+ console.log(`Removed the diffprism block from ${hookPath}`);
976
+ }
977
+ function removeMarkedBlock(contents) {
978
+ const lines = contents.split("\n");
979
+ const out = [];
980
+ let inside = false;
981
+ for (const line of lines) {
982
+ if (line.trim() === MARKER_START) {
983
+ inside = true;
984
+ continue;
985
+ }
986
+ if (line.trim() === MARKER_END) {
987
+ inside = false;
988
+ continue;
989
+ }
990
+ if (!inside) {
991
+ out.push(line);
992
+ }
993
+ }
994
+ return ensureTrailingNewline(out.join("\n").replace(/\n{3,}/g, "\n\n"));
995
+ }
996
+ function resolveMinLines(flags, cwd) {
997
+ const fromFlag = flags.minLines ? Number.parseInt(flags.minLines, 10) : NaN;
998
+ if (Number.isFinite(fromFlag) && fromFlag > 0) {
999
+ return fromFlag;
1000
+ }
1001
+ const configured = Number.parseInt(
1002
+ git(["config", "--get", "diffprism.gate-lines"], cwd) ?? "",
1003
+ 10
1004
+ );
1005
+ if (Number.isFinite(configured) && configured > 0) {
1006
+ return configured;
1007
+ }
1008
+ return DEFAULT_MIN_LINES;
1009
+ }
1010
+ function resolveHookPath(cwd) {
1011
+ const configured = git(["config", "--get", "core.hooksPath"], cwd);
1012
+ if (configured) {
1013
+ return path4.resolve(cwd, configured, "pre-commit");
1014
+ }
1015
+ const hooksDir = git(["rev-parse", "--git-path", "hooks"], cwd) ?? ".git/hooks";
1016
+ return path4.resolve(cwd, hooksDir, "pre-commit");
1017
+ }
1018
+ function git(args, cwd) {
1019
+ try {
1020
+ const out = execFileSync("git", args, {
1021
+ cwd,
1022
+ encoding: "utf8",
1023
+ stdio: ["ignore", "pipe", "ignore"]
1024
+ });
1025
+ const trimmed = out.trim();
1026
+ return trimmed === "" ? null : trimmed;
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function ensureTrailingNewline(text) {
1032
+ return text.endsWith("\n") ? text : `${text}
1033
+ `;
1034
+ }
1035
+ function message(err) {
1036
+ return err instanceof Error ? err.message : String(err);
1037
+ }
1038
+ function fail(text) {
1039
+ console.error(text);
1040
+ process.exit(1);
1041
+ }
1042
+
854
1043
  // cli/src/index.ts
855
1044
  var program = new Command();
856
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.45.0" : "0.0.0-dev");
1045
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.47.0" : "0.0.0-dev");
857
1046
  program.action(defaultAction);
858
1047
  program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
859
1048
  program.command("review [ref]").description("Open a browser-based diff review (local git ref or GitHub PR ref like owner/repo#123)").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--reasoning <text>", "Agent reasoning about the changes").option("--dev", "Use Vite dev server with HMR instead of static files").option("--post-to-github", "Automatically post review back to GitHub without prompting").action(review);
860
1049
  program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
1050
+ var hookCmd = program.command("hook").description("Git hook integration \u2014 gate commits on a human review");
1051
+ hookCmd.command("pre-commit").description("Run the review gate for the staged changes (called from a git hook)").option("--min-lines <n>", "Staged lines required to trigger a review (default: 120)").option("--dev", "Use Vite dev server").action((flags) => {
1052
+ void preCommitHook(flags);
1053
+ });
1054
+ hookCmd.command("install").description("Add the diffprism gate to this repo's pre-commit hook").action(() => {
1055
+ installHook();
1056
+ });
1057
+ hookCmd.command("uninstall").description("Remove the diffprism gate from this repo's pre-commit hook").action(() => {
1058
+ uninstallHook();
1059
+ });
861
1060
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
862
1061
  program.command("setup").description("Configure DiffPrism for Claude Code integration").option("--global", "Configure globally (skill + permissions, no git repo required)").option("--force", "Overwrite existing configuration files").option("--dev", "Use Vite dev server").option("--no-demo", "Skip the demo review after setup").action((flags) => {
863
1062
  setup(flags);
@@ -122,30 +122,45 @@ function buildDefaultSpawnCommand(options) {
122
122
  const thisDir = path2.dirname(thisFile);
123
123
  const workspaceRoot = path2.resolve(thisDir, "..", "..", "..");
124
124
  const devBin = path2.join(workspaceRoot, "cli", "bin", "diffprism.mjs");
125
- let binPath = "diffprism";
126
125
  if (fs2.existsSync(devBin)) {
127
- binPath = devBin;
128
- } else {
129
- let searchDir = thisDir;
130
- while (searchDir !== path2.dirname(searchDir)) {
131
- const candidate = path2.join(
132
- searchDir,
133
- "node_modules",
134
- ".bin",
135
- "diffprism"
126
+ return withDevFlag([process.execPath, devBin, "server", "--_daemon"], options);
127
+ }
128
+ let searchDir = thisDir;
129
+ while (searchDir !== path2.dirname(searchDir)) {
130
+ const ownBin = readOwnBinPath(searchDir);
131
+ if (ownBin) {
132
+ return withDevFlag(
133
+ [process.execPath, ownBin, "server", "--_daemon"],
134
+ options
136
135
  );
137
- if (fs2.existsSync(candidate)) {
138
- binPath = candidate;
139
- break;
140
- }
141
- searchDir = path2.dirname(searchDir);
142
136
  }
137
+ const shim = path2.join(searchDir, "node_modules", ".bin", "diffprism");
138
+ if (fs2.existsSync(shim)) {
139
+ return withDevFlag([shim, "server", "--_daemon"], options);
140
+ }
141
+ searchDir = path2.dirname(searchDir);
142
+ }
143
+ return withDevFlag(["diffprism", "server", "--_daemon"], options);
144
+ }
145
+ function withDevFlag(args, options) {
146
+ return options.dev ? [...args, "--dev"] : args;
147
+ }
148
+ function readOwnBinPath(dir) {
149
+ const manifestPath = path2.join(dir, "package.json");
150
+ if (!fs2.existsSync(manifestPath)) {
151
+ return null;
143
152
  }
144
- const args = [process.execPath, binPath, "server", "--_daemon"];
145
- if (options.dev) {
146
- args.push("--dev");
153
+ try {
154
+ const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
155
+ const entry = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.diffprism;
156
+ if (!entry) {
157
+ return null;
158
+ }
159
+ const resolved = path2.resolve(dir, entry);
160
+ return fs2.existsSync(resolved) ? resolved : null;
161
+ } catch {
162
+ return null;
147
163
  }
148
- return args;
149
164
  }
150
165
  async function submitReviewToServer(serverInfo, diffRef, options = {}) {
151
166
  const cwd = options.cwd ?? process.cwd();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ensureServer,
3
3
  submitReviewToServer
4
- } from "./chunk-RICVPPTE.js";
4
+ } from "./chunk-PQWU5NW4.js";
5
5
  import {
6
6
  parseDiff
7
7
  } from "./chunk-QGWYCEJN.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  demo
3
- } from "./chunk-RVI74JF5.js";
4
- import "./chunk-RICVPPTE.js";
3
+ } from "./chunk-TZ5SGWHP.js";
4
+ import "./chunk-PQWU5NW4.js";
5
5
  import "./chunk-QGWYCEJN.js";
6
6
  import "./chunk-DHCVZGHE.js";
7
7
  import "./chunk-JSBRDJBE.js";
@@ -6,7 +6,7 @@ import {
6
6
  ensureServer,
7
7
  isServerAlive,
8
8
  submitReviewToServer
9
- } from "./chunk-RICVPPTE.js";
9
+ } from "./chunk-PQWU5NW4.js";
10
10
  import {
11
11
  getDiff
12
12
  } from "./chunk-QGWYCEJN.js";
@@ -144,7 +144,7 @@ async function handlePrReview(pr, options) {
144
144
  async function startMcpServer() {
145
145
  const server = new McpServer({
146
146
  name: "diffprism",
147
- version: true ? "0.45.0" : "0.0.0-dev"
147
+ version: true ? "0.47.0" : "0.0.0-dev"
148
148
  });
149
149
  server.tool(
150
150
  "open_review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {