diffprism 0.45.0 → 0.48.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,19 @@ import {
|
|
|
5
5
|
} from "./chunk-24B33UN6.js";
|
|
6
6
|
import {
|
|
7
7
|
demo
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SWSRPUXO.js";
|
|
9
9
|
import {
|
|
10
|
+
describeVersion,
|
|
10
11
|
ensureServer,
|
|
12
|
+
getBuildInfo,
|
|
11
13
|
isServerAlive,
|
|
12
14
|
readServerFile,
|
|
13
15
|
startGlobalServer,
|
|
14
16
|
submitReviewToServer
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import
|
|
17
|
+
} from "./chunk-AJGESVAM.js";
|
|
18
|
+
import {
|
|
19
|
+
getDiff
|
|
20
|
+
} from "./chunk-QGWYCEJN.js";
|
|
17
21
|
import "./chunk-DHCVZGHE.js";
|
|
18
22
|
import "./chunk-JSBRDJBE.js";
|
|
19
23
|
|
|
@@ -39,21 +43,26 @@ async function review(ref, flags) {
|
|
|
39
43
|
await reviewLocalFlow(diffRef, flags);
|
|
40
44
|
}
|
|
41
45
|
} catch (err) {
|
|
42
|
-
const
|
|
43
|
-
console.error(`Error: ${
|
|
46
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
47
|
+
console.error(`Error: ${message2}`);
|
|
44
48
|
process.exit(1);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
async function reviewLocalFlow(diffRef, flags) {
|
|
48
52
|
const serverInfo = await ensureServer({ dev: flags.dev });
|
|
49
|
-
console.
|
|
53
|
+
console.error("Opening review in browser...");
|
|
50
54
|
const { result } = await submitReviewToServer(serverInfo, diffRef, {
|
|
51
55
|
title: flags.title,
|
|
52
56
|
cwd: process.cwd(),
|
|
53
57
|
diffRef
|
|
54
58
|
});
|
|
55
59
|
console.log(JSON.stringify(result, null, 2));
|
|
56
|
-
|
|
60
|
+
if (!result) {
|
|
61
|
+
console.error("No review result was returned.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const approved = result.decision === "approved" || result.decision === "approved_with_comments";
|
|
65
|
+
process.exit(approved ? 0 : 1);
|
|
57
66
|
}
|
|
58
67
|
async function reviewPrFlow(pr, flags) {
|
|
59
68
|
const { owner, repo, number } = parsePrRef(pr);
|
|
@@ -370,7 +379,7 @@ async function setupInteractive(flags) {
|
|
|
370
379
|
}
|
|
371
380
|
async function runDemo(dev) {
|
|
372
381
|
console.log("");
|
|
373
|
-
const { demo: demo2 } = await import("./demo-
|
|
382
|
+
const { demo: demo2 } = await import("./demo-7GBBKPPB.js");
|
|
374
383
|
await demo2({ dev });
|
|
375
384
|
}
|
|
376
385
|
async function setupBatch(flags) {
|
|
@@ -703,9 +712,9 @@ async function server(flags) {
|
|
|
703
712
|
await new Promise(() => {
|
|
704
713
|
});
|
|
705
714
|
} catch (err) {
|
|
706
|
-
const
|
|
715
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
707
716
|
if (!isDaemon) {
|
|
708
|
-
console.error(`Error starting server: ${
|
|
717
|
+
console.error(`Error starting server: ${message2}`);
|
|
709
718
|
}
|
|
710
719
|
process.exit(1);
|
|
711
720
|
}
|
|
@@ -764,6 +773,10 @@ async function serverStatus() {
|
|
|
764
773
|
console.log(` PID: ${status.pid}`);
|
|
765
774
|
console.log(` Sessions: ${status.sessions}`);
|
|
766
775
|
console.log(` Uptime: ${Math.floor(status.uptime)}s`);
|
|
776
|
+
const build = getBuildInfo();
|
|
777
|
+
if (build.dev) {
|
|
778
|
+
console.log(` Build: dev \u2014 ${build.root}`);
|
|
779
|
+
}
|
|
767
780
|
if (status.sessions > 0) {
|
|
768
781
|
const sessionsResponse = await fetch(
|
|
769
782
|
`http://localhost:${info.httpPort}/api/reviews`,
|
|
@@ -778,8 +791,8 @@ async function serverStatus() {
|
|
|
778
791
|
}
|
|
779
792
|
}
|
|
780
793
|
} catch (err) {
|
|
781
|
-
const
|
|
782
|
-
console.error(`Error checking server status: ${
|
|
794
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
795
|
+
console.error(`Error checking server status: ${message2}`);
|
|
783
796
|
process.exit(1);
|
|
784
797
|
}
|
|
785
798
|
}
|
|
@@ -813,7 +826,7 @@ async function fetchStatus(httpPort) {
|
|
|
813
826
|
return await response.json();
|
|
814
827
|
}
|
|
815
828
|
function printStatus(status, httpPort) {
|
|
816
|
-
const version = true ? "0.
|
|
829
|
+
const version = true ? "0.48.0" : "0.0.0-dev";
|
|
817
830
|
console.log(`
|
|
818
831
|
DiffPrism v${version}
|
|
819
832
|
`);
|
|
@@ -845,19 +858,215 @@ async function defaultAction() {
|
|
|
845
858
|
await open(status.uiUrl);
|
|
846
859
|
}
|
|
847
860
|
} catch (err) {
|
|
848
|
-
const
|
|
849
|
-
console.error(`Error fetching server status: ${
|
|
861
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
862
|
+
console.error(`Error fetching server status: ${message2}`);
|
|
850
863
|
process.exit(1);
|
|
851
864
|
}
|
|
852
865
|
}
|
|
853
866
|
|
|
867
|
+
// cli/src/commands/hook.ts
|
|
868
|
+
import { execFileSync } from "child_process";
|
|
869
|
+
import fs4 from "fs";
|
|
870
|
+
import path4 from "path";
|
|
871
|
+
var DEFAULT_MIN_LINES = 120;
|
|
872
|
+
var MARKER_START = "# >>> diffprism >>>";
|
|
873
|
+
var MARKER_END = "# <<< diffprism <<<";
|
|
874
|
+
var HOOK_LINE = "diffprism hook pre-commit || exit 1";
|
|
875
|
+
async function preCommitHook(flags = {}) {
|
|
876
|
+
const cwd = process.cwd();
|
|
877
|
+
const minLines = resolveMinLines(flags, cwd);
|
|
878
|
+
let changedLines;
|
|
879
|
+
try {
|
|
880
|
+
const { diffSet } = getDiff("staged", { cwd });
|
|
881
|
+
changedLines = diffSet.files.reduce(
|
|
882
|
+
(total, file) => total + file.additions + file.deletions,
|
|
883
|
+
0
|
|
884
|
+
);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
fail(`Could not read the staged diff: ${message(err)}`);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (changedLines === 0) {
|
|
890
|
+
process.exit(0);
|
|
891
|
+
}
|
|
892
|
+
if (changedLines < minLines) {
|
|
893
|
+
process.exit(0);
|
|
894
|
+
}
|
|
895
|
+
console.error(
|
|
896
|
+
`${changedLines} staged lines (gate at ${minLines}) \u2014 opening DiffPrism review...`
|
|
897
|
+
);
|
|
898
|
+
let review2 = null;
|
|
899
|
+
try {
|
|
900
|
+
const serverInfo = await ensureServer({ dev: flags.dev });
|
|
901
|
+
const { result } = await submitReviewToServer(serverInfo, "staged", {
|
|
902
|
+
cwd,
|
|
903
|
+
diffRef: "staged",
|
|
904
|
+
title: "Pre-commit review"
|
|
905
|
+
});
|
|
906
|
+
review2 = result;
|
|
907
|
+
} catch (err) {
|
|
908
|
+
fail(`DiffPrism could not run the review: ${message(err)}`);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const decision = review2?.decision;
|
|
912
|
+
if (decision === "approved" || decision === "approved_with_comments") {
|
|
913
|
+
console.error(`Review: ${decision} \u2014 proceeding.`);
|
|
914
|
+
process.exit(0);
|
|
915
|
+
}
|
|
916
|
+
if (decision === "changes_requested") {
|
|
917
|
+
printComments(review2?.comments ?? []);
|
|
918
|
+
fail("Commit blocked: the review requested changes.");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (decision === "dismissed") {
|
|
922
|
+
fail("Commit blocked: the review was dismissed without a decision.");
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
fail(
|
|
926
|
+
`Commit blocked: no review decision was returned (got ${String(decision)}).`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
function printComments(comments) {
|
|
930
|
+
if (comments.length === 0) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
console.error("");
|
|
934
|
+
for (const c of comments) {
|
|
935
|
+
console.error(` ${c.file}:${c.line} [${c.type}] ${c.body}`);
|
|
936
|
+
}
|
|
937
|
+
console.error("");
|
|
938
|
+
}
|
|
939
|
+
function installHook() {
|
|
940
|
+
const hookPath = resolveHookPath(process.cwd());
|
|
941
|
+
const existing = fs4.existsSync(hookPath) ? fs4.readFileSync(hookPath, "utf8") : "";
|
|
942
|
+
if (existing.includes(MARKER_START)) {
|
|
943
|
+
console.log(`Already installed in ${hookPath}`);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const block = `${MARKER_START}
|
|
947
|
+
${HOOK_LINE}
|
|
948
|
+
${MARKER_END}
|
|
949
|
+
`;
|
|
950
|
+
const base = existing === "" ? "#!/bin/sh\n" : ensureTrailingNewline(existing);
|
|
951
|
+
const created = existing === "";
|
|
952
|
+
fs4.mkdirSync(path4.dirname(hookPath), { recursive: true });
|
|
953
|
+
fs4.writeFileSync(hookPath, `${base}
|
|
954
|
+
${block}`);
|
|
955
|
+
fs4.chmodSync(hookPath, 493);
|
|
956
|
+
console.log(`${created ? "Created" : "Updated"} ${hookPath}`);
|
|
957
|
+
console.log(
|
|
958
|
+
`Staged changes of ${DEFAULT_MIN_LINES}+ lines now open a review before the commit lands.`
|
|
959
|
+
);
|
|
960
|
+
console.log("Tune with: git config diffprism.gate-lines <n>");
|
|
961
|
+
console.log("Remove with: diffprism hook uninstall");
|
|
962
|
+
}
|
|
963
|
+
function uninstallHook() {
|
|
964
|
+
const hookPath = resolveHookPath(process.cwd());
|
|
965
|
+
if (!fs4.existsSync(hookPath)) {
|
|
966
|
+
console.log("Nothing to remove \u2014 no hook file.");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const existing = fs4.readFileSync(hookPath, "utf8");
|
|
970
|
+
if (!existing.includes(MARKER_START)) {
|
|
971
|
+
console.log(`Nothing to remove \u2014 no diffprism block in ${hookPath}`);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const cleaned = removeMarkedBlock(existing);
|
|
975
|
+
if (/^\s*(#![^\n]*)?\s*$/.test(cleaned)) {
|
|
976
|
+
fs4.rmSync(hookPath);
|
|
977
|
+
console.log(`Removed ${hookPath} (it contained nothing else).`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
fs4.writeFileSync(hookPath, cleaned);
|
|
981
|
+
console.log(`Removed the diffprism block from ${hookPath}`);
|
|
982
|
+
}
|
|
983
|
+
function removeMarkedBlock(contents) {
|
|
984
|
+
const lines = contents.split("\n");
|
|
985
|
+
const out = [];
|
|
986
|
+
let inside = false;
|
|
987
|
+
for (const line of lines) {
|
|
988
|
+
if (line.trim() === MARKER_START) {
|
|
989
|
+
inside = true;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (line.trim() === MARKER_END) {
|
|
993
|
+
inside = false;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (!inside) {
|
|
997
|
+
out.push(line);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return ensureTrailingNewline(out.join("\n").replace(/\n{3,}/g, "\n\n"));
|
|
1001
|
+
}
|
|
1002
|
+
function resolveMinLines(flags, cwd) {
|
|
1003
|
+
const fromFlag = flags.minLines ? Number.parseInt(flags.minLines, 10) : NaN;
|
|
1004
|
+
if (Number.isFinite(fromFlag) && fromFlag > 0) {
|
|
1005
|
+
return fromFlag;
|
|
1006
|
+
}
|
|
1007
|
+
const configured = Number.parseInt(
|
|
1008
|
+
git(["config", "--get", "diffprism.gate-lines"], cwd) ?? "",
|
|
1009
|
+
10
|
|
1010
|
+
);
|
|
1011
|
+
if (Number.isFinite(configured) && configured > 0) {
|
|
1012
|
+
return configured;
|
|
1013
|
+
}
|
|
1014
|
+
return DEFAULT_MIN_LINES;
|
|
1015
|
+
}
|
|
1016
|
+
function resolveHookPath(cwd) {
|
|
1017
|
+
const configured = git(["config", "--get", "core.hooksPath"], cwd);
|
|
1018
|
+
if (configured) {
|
|
1019
|
+
return path4.resolve(cwd, configured, "pre-commit");
|
|
1020
|
+
}
|
|
1021
|
+
const hooksDir = git(["rev-parse", "--git-path", "hooks"], cwd) ?? ".git/hooks";
|
|
1022
|
+
return path4.resolve(cwd, hooksDir, "pre-commit");
|
|
1023
|
+
}
|
|
1024
|
+
function git(args, cwd) {
|
|
1025
|
+
try {
|
|
1026
|
+
const out = execFileSync("git", args, {
|
|
1027
|
+
cwd,
|
|
1028
|
+
encoding: "utf8",
|
|
1029
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1030
|
+
});
|
|
1031
|
+
const trimmed = out.trim();
|
|
1032
|
+
return trimmed === "" ? null : trimmed;
|
|
1033
|
+
} catch {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function ensureTrailingNewline(text) {
|
|
1038
|
+
return text.endsWith("\n") ? text : `${text}
|
|
1039
|
+
`;
|
|
1040
|
+
}
|
|
1041
|
+
function message(err) {
|
|
1042
|
+
return err instanceof Error ? err.message : String(err);
|
|
1043
|
+
}
|
|
1044
|
+
function fail(text) {
|
|
1045
|
+
console.error(text);
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
854
1049
|
// cli/src/index.ts
|
|
855
1050
|
var program = new Command();
|
|
856
|
-
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(
|
|
1051
|
+
program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(
|
|
1052
|
+
describeVersion(
|
|
1053
|
+
true ? "0.48.0" : "0.0.0-dev"
|
|
1054
|
+
)
|
|
1055
|
+
);
|
|
857
1056
|
program.action(defaultAction);
|
|
858
1057
|
program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
|
|
859
1058
|
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
1059
|
program.command("review-pr <pr>", { hidden: true }).action((pr, flags) => review(pr, flags));
|
|
1060
|
+
var hookCmd = program.command("hook").description("Git hook integration \u2014 gate commits on a human review");
|
|
1061
|
+
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) => {
|
|
1062
|
+
void preCommitHook(flags);
|
|
1063
|
+
});
|
|
1064
|
+
hookCmd.command("install").description("Add the diffprism gate to this repo's pre-commit hook").action(() => {
|
|
1065
|
+
installHook();
|
|
1066
|
+
});
|
|
1067
|
+
hookCmd.command("uninstall").description("Remove the diffprism gate from this repo's pre-commit hook").action(() => {
|
|
1068
|
+
uninstallHook();
|
|
1069
|
+
});
|
|
861
1070
|
program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
|
|
862
1071
|
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
1072
|
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
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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);
|
|
143
142
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
152
|
+
}
|
|
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();
|
|
@@ -251,6 +266,27 @@ async function submitReviewToServer(serverInfo, diffRef, options = {}) {
|
|
|
251
266
|
throw new Error("Review timed out waiting for submission.");
|
|
252
267
|
}
|
|
253
268
|
|
|
269
|
+
// packages/core/src/build-info.ts
|
|
270
|
+
import fs3 from "fs";
|
|
271
|
+
import path3 from "path";
|
|
272
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
273
|
+
function getBuildInfo() {
|
|
274
|
+
let dir = path3.dirname(fileURLToPath2(import.meta.url));
|
|
275
|
+
while (dir !== path3.dirname(dir)) {
|
|
276
|
+
if (fs3.existsSync(path3.join(dir, "package.json"))) {
|
|
277
|
+
if (fs3.existsSync(path3.join(dir, ".git"))) {
|
|
278
|
+
return { dev: true, root: dir };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
dir = path3.dirname(dir);
|
|
282
|
+
}
|
|
283
|
+
return { dev: false, root: null };
|
|
284
|
+
}
|
|
285
|
+
function describeVersion(version) {
|
|
286
|
+
const info = getBuildInfo();
|
|
287
|
+
return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
|
|
288
|
+
}
|
|
289
|
+
|
|
254
290
|
// packages/core/src/diff-utils.ts
|
|
255
291
|
import { createHash } from "crypto";
|
|
256
292
|
function hashDiff(rawDiff) {
|
|
@@ -355,13 +391,13 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
355
391
|
import getPort from "get-port";
|
|
356
392
|
import open from "open";
|
|
357
393
|
import { WebSocketServer, WebSocket } from "ws";
|
|
358
|
-
import
|
|
394
|
+
import fs6 from "fs";
|
|
359
395
|
|
|
360
396
|
// packages/core/src/ui-server.ts
|
|
361
397
|
import http from "http";
|
|
362
|
-
import
|
|
363
|
-
import
|
|
364
|
-
import { fileURLToPath as
|
|
398
|
+
import fs4 from "fs";
|
|
399
|
+
import path4 from "path";
|
|
400
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
365
401
|
var MIME_TYPES = {
|
|
366
402
|
".html": "text/html",
|
|
367
403
|
".js": "application/javascript",
|
|
@@ -374,15 +410,15 @@ var MIME_TYPES = {
|
|
|
374
410
|
".woff2": "font/woff2"
|
|
375
411
|
};
|
|
376
412
|
function resolveUiDist() {
|
|
377
|
-
const thisFile =
|
|
378
|
-
const thisDir =
|
|
379
|
-
const publishedUiDist =
|
|
380
|
-
if (
|
|
413
|
+
const thisFile = fileURLToPath3(import.meta.url);
|
|
414
|
+
const thisDir = path4.dirname(thisFile);
|
|
415
|
+
const publishedUiDist = path4.resolve(thisDir, "..", "ui-dist");
|
|
416
|
+
if (fs4.existsSync(path4.join(publishedUiDist, "index.html"))) {
|
|
381
417
|
return publishedUiDist;
|
|
382
418
|
}
|
|
383
|
-
const workspaceRoot =
|
|
384
|
-
const devUiDist =
|
|
385
|
-
if (
|
|
419
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
420
|
+
const devUiDist = path4.join(workspaceRoot, "packages", "ui", "dist");
|
|
421
|
+
if (fs4.existsSync(path4.join(devUiDist, "index.html"))) {
|
|
386
422
|
return devUiDist;
|
|
387
423
|
}
|
|
388
424
|
throw new Error(
|
|
@@ -390,11 +426,11 @@ function resolveUiDist() {
|
|
|
390
426
|
);
|
|
391
427
|
}
|
|
392
428
|
function resolveUiRoot() {
|
|
393
|
-
const thisFile =
|
|
394
|
-
const thisDir =
|
|
395
|
-
const workspaceRoot =
|
|
396
|
-
const uiRoot =
|
|
397
|
-
if (
|
|
429
|
+
const thisFile = fileURLToPath3(import.meta.url);
|
|
430
|
+
const thisDir = path4.dirname(thisFile);
|
|
431
|
+
const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
|
|
432
|
+
const uiRoot = path4.join(workspaceRoot, "packages", "ui");
|
|
433
|
+
if (fs4.existsSync(path4.join(uiRoot, "index.html"))) {
|
|
398
434
|
return uiRoot;
|
|
399
435
|
}
|
|
400
436
|
throw new Error(
|
|
@@ -414,14 +450,14 @@ async function startViteDevServer(uiRoot, port, silent) {
|
|
|
414
450
|
function createStaticServer(distPath, port) {
|
|
415
451
|
const server = http.createServer((req, res) => {
|
|
416
452
|
const urlPath = req.url?.split("?")[0] ?? "/";
|
|
417
|
-
let filePath =
|
|
418
|
-
if (!
|
|
419
|
-
filePath =
|
|
453
|
+
let filePath = path4.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
454
|
+
if (!fs4.existsSync(filePath)) {
|
|
455
|
+
filePath = path4.join(distPath, "index.html");
|
|
420
456
|
}
|
|
421
|
-
const ext =
|
|
457
|
+
const ext = path4.extname(filePath);
|
|
422
458
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
423
459
|
try {
|
|
424
|
-
const content =
|
|
460
|
+
const content = fs4.readFileSync(filePath);
|
|
425
461
|
res.writeHead(200, { "Content-Type": contentType });
|
|
426
462
|
res.end(content);
|
|
427
463
|
} catch {
|
|
@@ -436,22 +472,22 @@ function createStaticServer(distPath, port) {
|
|
|
436
472
|
}
|
|
437
473
|
|
|
438
474
|
// packages/core/src/review-history.ts
|
|
439
|
-
import
|
|
440
|
-
import
|
|
475
|
+
import fs5 from "fs";
|
|
476
|
+
import path5 from "path";
|
|
441
477
|
import { randomUUID } from "crypto";
|
|
442
478
|
function generateEntryId() {
|
|
443
479
|
return randomUUID();
|
|
444
480
|
}
|
|
445
481
|
function getHistoryPath(projectDir) {
|
|
446
|
-
return
|
|
482
|
+
return path5.join(projectDir, ".diffprism", "history", "reviews.json");
|
|
447
483
|
}
|
|
448
484
|
function readHistory(projectDir) {
|
|
449
485
|
const filePath = getHistoryPath(projectDir);
|
|
450
|
-
if (!
|
|
486
|
+
if (!fs5.existsSync(filePath)) {
|
|
451
487
|
return { version: 1, entries: [] };
|
|
452
488
|
}
|
|
453
489
|
try {
|
|
454
|
-
const raw =
|
|
490
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
455
491
|
const parsed = JSON.parse(raw);
|
|
456
492
|
return parsed;
|
|
457
493
|
} catch {
|
|
@@ -460,14 +496,14 @@ function readHistory(projectDir) {
|
|
|
460
496
|
}
|
|
461
497
|
function appendHistory(projectDir, entry) {
|
|
462
498
|
const filePath = getHistoryPath(projectDir);
|
|
463
|
-
const dir =
|
|
464
|
-
if (!
|
|
465
|
-
|
|
499
|
+
const dir = path5.dirname(filePath);
|
|
500
|
+
if (!fs5.existsSync(dir)) {
|
|
501
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
466
502
|
}
|
|
467
503
|
const history = readHistory(projectDir);
|
|
468
504
|
history.entries.push(entry);
|
|
469
505
|
history.entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
470
|
-
|
|
506
|
+
fs5.writeFileSync(filePath, JSON.stringify(history, null, 2) + "\n");
|
|
471
507
|
}
|
|
472
508
|
function getRecentHistory(projectDir, limit = 50) {
|
|
473
509
|
const history = readHistory(projectDir);
|
|
@@ -671,12 +707,12 @@ function recordReviewHistory(session, result) {
|
|
|
671
707
|
function isInsideGitRepo(dirPath) {
|
|
672
708
|
let current = dirPath;
|
|
673
709
|
while (current !== "/") {
|
|
674
|
-
if (
|
|
710
|
+
if (fs6.existsSync(`${current}/.git`)) return true;
|
|
675
711
|
const parent = current.replace(/\/[^/]+$/, "") || "/";
|
|
676
712
|
if (parent === current) break;
|
|
677
713
|
current = parent;
|
|
678
714
|
}
|
|
679
|
-
return
|
|
715
|
+
return fs6.existsSync(`${current}/.git`);
|
|
680
716
|
}
|
|
681
717
|
async function handleApiRequest(req, res) {
|
|
682
718
|
const method = req.method ?? "GET";
|
|
@@ -787,7 +823,7 @@ async function handleApiRequest(req, res) {
|
|
|
787
823
|
return true;
|
|
788
824
|
}
|
|
789
825
|
try {
|
|
790
|
-
const stat =
|
|
826
|
+
const stat = fs6.statSync(projectPath);
|
|
791
827
|
if (!stat.isDirectory()) {
|
|
792
828
|
jsonResponse(res, 400, { error: "Path is not a directory" });
|
|
793
829
|
return true;
|
|
@@ -946,7 +982,7 @@ async function handleApiRequest(req, res) {
|
|
|
946
982
|
if (parsedUrl.pathname === "/api/fs/list") {
|
|
947
983
|
const dirPath = parsedUrl.searchParams.get("path") || process.cwd();
|
|
948
984
|
try {
|
|
949
|
-
const stat =
|
|
985
|
+
const stat = fs6.statSync(dirPath);
|
|
950
986
|
if (!stat.isDirectory()) {
|
|
951
987
|
jsonResponse(res, 400, { error: "Not a directory" });
|
|
952
988
|
return true;
|
|
@@ -956,13 +992,13 @@ async function handleApiRequest(req, res) {
|
|
|
956
992
|
return true;
|
|
957
993
|
}
|
|
958
994
|
try {
|
|
959
|
-
const entries =
|
|
995
|
+
const entries = fs6.readdirSync(dirPath, { withFileTypes: true });
|
|
960
996
|
const dirs = [];
|
|
961
997
|
for (const entry of entries) {
|
|
962
998
|
if (!entry.isDirectory()) continue;
|
|
963
999
|
if (entry.name.startsWith(".")) continue;
|
|
964
1000
|
const fullPath = `${dirPath}/${entry.name}`;
|
|
965
|
-
const isGitRepo2 =
|
|
1001
|
+
const isGitRepo2 = fs6.existsSync(`${fullPath}/.git`);
|
|
966
1002
|
dirs.push({ name: entry.name, path: fullPath, isGitRepo: isGitRepo2 });
|
|
967
1003
|
}
|
|
968
1004
|
dirs.sort((a, b) => {
|
|
@@ -1550,5 +1586,7 @@ export {
|
|
|
1550
1586
|
isServerAlive,
|
|
1551
1587
|
startGlobalServer,
|
|
1552
1588
|
ensureServer,
|
|
1553
|
-
submitReviewToServer
|
|
1589
|
+
submitReviewToServer,
|
|
1590
|
+
getBuildInfo,
|
|
1591
|
+
describeVersion
|
|
1554
1592
|
};
|
package/dist/mcp-server.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ensureServer,
|
|
7
7
|
isServerAlive,
|
|
8
8
|
submitReviewToServer
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-AJGESVAM.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.
|
|
147
|
+
version: true ? "0.48.0" : "0.0.0-dev"
|
|
148
148
|
});
|
|
149
149
|
server.tool(
|
|
150
150
|
"open_review",
|