diffprism 1.2.0 → 1.3.1

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
@@ -218,6 +218,26 @@ pnpm run build
218
218
  pnpm cli review --staged # Run CLI from source
219
219
  ```
220
220
 
221
+ ### Dogfooding a checkout
222
+
223
+ To try unreleased changes in another project without publishing, point your global `diffprism` at a checkout:
224
+
225
+ ```bash
226
+ git clone https://github.com/CodeJonesW/diffprism.git diffprism-dogfood
227
+ cd diffprism-dogfood && pnpm install && pnpm build && npm link
228
+ diffprism --version # 1.2.0 (dev build — /path/to/diffprism-dogfood)
229
+ ```
230
+
231
+ Then, for each change you want to try:
232
+
233
+ ```bash
234
+ git fetch && git checkout <branch> && git pull && pnpm build
235
+ ```
236
+
237
+ The next `diffprism` command — a review, the commit gate — replaces the running server with the new build, unless a review is open in it. The dashboard picks up a rebuilt UI on reload. Go back to the release with `npm install -g diffprism@latest`.
238
+
239
+ A server is only ever replaced by a **newer** build. Claude Code keeps `diffprism serve` running on the build it started with until you restart it, so restart Claude Code to put its MCP tools on the new build too.
240
+
221
241
  ### Project Structure
222
242
 
223
243
  ```
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-EPU4F7WT.js";
6
6
  import {
7
7
  demo
8
- } from "./chunk-FONA4U6B.js";
8
+ } from "./chunk-PJYDCLBP.js";
9
9
  import {
10
10
  COMMIT_GATE_DIFF_REF,
11
11
  DEFAULT_DIFF_REF,
@@ -26,7 +26,7 @@ import {
26
26
  recordError,
27
27
  startGlobalServer,
28
28
  submitReviewToServer
29
- } from "./chunk-F6O5YP3I.js";
29
+ } from "./chunk-35YVBTBH.js";
30
30
  import {
31
31
  getDiff
32
32
  } from "./chunk-3GMPE2ZR.js";
@@ -79,6 +79,9 @@ ${REPORT_HINT}`);
79
79
  }
80
80
  const where = data.annotation ? ` on ${data.annotation.file}:${data.annotation.line}` : "";
81
81
  console.log(`Replied${where}. It shows in the review now.`);
82
+ console.log(
83
+ "The review is still open. Go back to waiting for the decision now: run the same command that opened it again (`git commit` or `diffprism review`). It returns the decision, or the reviewer's next question. Don't ask them in the terminal."
84
+ );
82
85
  }
83
86
  function fail(message2) {
84
87
  console.error(message2);
@@ -136,7 +139,7 @@ ${REPORT_HINT}`);
136
139
  if (err instanceof ReviewerAskedError) {
137
140
  printQuestions(err.sessionId, err.threads);
138
141
  fail2(
139
- "Commit blocked: the reviewer asked you something before deciding. Answer each question with the command under it \u2014 change the code too if that's what they asked for \u2014 then run git commit again. The review stays open and the decision still comes."
142
+ "Commit blocked: the reviewer asked you something before deciding. Answer each question with the command under it \u2014 change the code too if that's what they asked for \u2014 then run git commit again straight away to keep waiting. The review stays open, the reviewer may ask more, and the decision still comes. Don't stop to ask them in the terminal."
140
143
  );
141
144
  return;
142
145
  }
@@ -369,7 +372,7 @@ async function reviewLocalFlow(diffRef, flags) {
369
372
  if (err instanceof ReviewerAskedError) {
370
373
  printQuestions(err.sessionId, err.threads);
371
374
  console.error(
372
- "The reviewer asked something before deciding. Answer each question with the command under it, then run diffprism review again \u2014 the review stays open."
375
+ "The reviewer asked something before deciding. Answer each question with the command under it, then run diffprism review again straight away to keep waiting \u2014 the review stays open, and the reviewer may ask more. Don't stop to ask them in the terminal."
373
376
  );
374
377
  process.exit(1);
375
378
  }
@@ -716,7 +719,7 @@ async function setupInteractive(flags) {
716
719
  }
717
720
  async function runDemo(dev) {
718
721
  console.log("");
719
- const { demo: demo2 } = await import("./demo-VNWYEDZ3.js");
722
+ const { demo: demo2 } = await import("./demo-WIP5MSCH.js");
720
723
  await demo2({ dev });
721
724
  }
722
725
  async function setupBatch(flags) {
@@ -79,15 +79,13 @@ async function isServerAlive() {
79
79
  }
80
80
  }
81
81
 
82
- // packages/core/src/diff-scope.ts
83
- var DEFAULT_DIFF_REF = "working-copy";
84
- var COMMIT_GATE_DIFF_REF = "staged";
85
- var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
86
-
87
82
  // packages/core/src/build-info.ts
88
83
  import fs2 from "fs";
89
84
  import path2 from "path";
90
85
  import { fileURLToPath } from "url";
86
+ function builtAt() {
87
+ return true ? 1789610123399 : null;
88
+ }
91
89
  function getBuildInfo() {
92
90
  let dir = path2.dirname(fileURLToPath(import.meta.url));
93
91
  while (dir !== path2.dirname(dir)) {
@@ -112,6 +110,11 @@ function describeVersion(version) {
112
110
  return info.dev ? `${version} (dev build \u2014 ${info.root})` : version;
113
111
  }
114
112
 
113
+ // packages/core/src/diff-scope.ts
114
+ var DEFAULT_DIFF_REF = "working-copy";
115
+ var COMMIT_GATE_DIFF_REF = "staged";
116
+ var DIFF_REF_DESCRIPTION = 'Which changes to review. "working-copy" (the default): everything not yet committed, staged and unstaged shown as separate groups. "staged": only what the next commit would contain. "unstaged": only edits not yet staged. Or a ref range such as "HEAD~3..HEAD" or "main..feature".';
117
+
115
118
  // packages/core/src/feedback.ts
116
119
  import fs3 from "fs";
117
120
  import os2 from "os";
@@ -119,7 +122,7 @@ import path3 from "path";
119
122
  var ISSUES_NEW_URL = "https://github.com/CodeJonesW/diffprism/issues/new";
120
123
  var MAX_ERROR_CHARS = 1500;
121
124
  function currentVersion() {
122
- return true ? "1.2.0" : "0.0.0-dev";
125
+ return true ? "1.3.1" : "0.0.0-dev";
123
126
  }
124
127
  function describeEnvironment() {
125
128
  return {
@@ -226,7 +229,22 @@ import { fileURLToPath as fileURLToPath2 } from "url";
226
229
  async function ensureServer(options = {}) {
227
230
  const existing = await isServerAlive();
228
231
  if (existing) {
229
- return existing;
232
+ const decision = await decideOnRunningServer(existing, builtAt());
233
+ if (decision.action === "keep") {
234
+ return existing;
235
+ }
236
+ if (decision.action === "keep-busy") {
237
+ if (!options.silent) {
238
+ console.error(
239
+ `The DiffPrism server (PID ${existing.pid}) runs an older build, but ${decision.openReviews} review${decision.openReviews === 1 ? " is" : "s are"} open in it, so it stays up. Finish or close ${decision.openReviews === 1 ? "it" : "them"} and the next command starts the new build \u2014 or run \`diffprism server stop\`.`
240
+ );
241
+ }
242
+ return existing;
243
+ }
244
+ if (!options.silent) {
245
+ console.error(`Replacing the DiffPrism server (PID ${existing.pid}): it runs an older build than this command.`);
246
+ }
247
+ await stopServer(existing);
230
248
  }
231
249
  const spawnArgs = options.spawnCommand ?? buildDefaultSpawnCommand(options);
232
250
  const logDir = path4.join(os3.homedir(), ".diffprism");
@@ -256,6 +274,44 @@ async function ensureServer(options = {}) {
256
274
  `DiffPrism server failed to start within ${timeoutMs / 1e3}s. Check logs at ${logPath}`
257
275
  );
258
276
  }
277
+ async function decideOnRunningServer(server, ownBuiltAt) {
278
+ if (ownBuiltAt === null) return { action: "keep" };
279
+ if (server.builtAt !== void 0 && server.builtAt >= ownBuiltAt) return { action: "keep" };
280
+ const response = await fetch(`http://localhost:${server.httpPort}/api/reviews`);
281
+ if (!response.ok) {
282
+ throw new Error(`Could not list reviews on the running DiffPrism server: it returned ${response.status}`);
283
+ }
284
+ const { sessions: sessions2 } = await response.json();
285
+ const openReviews = sessions2.filter((s) => s.status === "pending" || s.status === "in_review").length;
286
+ return openReviews > 0 ? { action: "keep-busy", openReviews } : { action: "replace" };
287
+ }
288
+ async function stopServer(server, timeoutMs = 5e3) {
289
+ try {
290
+ process.kill(server.pid, "SIGTERM");
291
+ } catch {
292
+ }
293
+ const start = Date.now();
294
+ while (Date.now() - start < timeoutMs) {
295
+ if (!isProcessRunning(server.pid)) {
296
+ if (readServerFile()?.pid === server.pid) {
297
+ removeServerFile();
298
+ }
299
+ return;
300
+ }
301
+ await new Promise((resolve) => setTimeout(resolve, 100));
302
+ }
303
+ throw new Error(
304
+ `The old DiffPrism server (PID ${server.pid}) did not stop within ${timeoutMs / 1e3}s. Stop it with \`diffprism server stop\` and try again.`
305
+ );
306
+ }
307
+ function isProcessRunning(pid) {
308
+ try {
309
+ process.kill(pid, 0);
310
+ return true;
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
259
315
  function buildDefaultSpawnCommand(options) {
260
316
  const thisFile = fileURLToPath2(import.meta.url);
261
317
  const thisDir = path4.dirname(thisFile);
@@ -861,6 +917,7 @@ var clientSessions = /* @__PURE__ */ new Map();
861
917
  var sessionWatchers = /* @__PURE__ */ new Map();
862
918
  var watchSchedule = DEFAULT_WATCH_SCHEDULE;
863
919
  var reopenBrowserIfNeeded = null;
920
+ var DASHBOARD_RECONNECT_GRACE_MS = 3e3;
864
921
  var serverUiUrl = null;
865
922
  function toSummary(session) {
866
923
  const { payload } = session;
@@ -1741,6 +1798,8 @@ async function startGlobalServer(options = {}) {
1741
1798
  const {
1742
1799
  httpPort: preferredHttpPort = 24680,
1743
1800
  wsPort: preferredWsPort = 24681,
1801
+ uiPort: preferredUiPort = 24682,
1802
+ reconnectGraceMs = DASHBOARD_RECONNECT_GRACE_MS,
1744
1803
  silent = false,
1745
1804
  dev = false,
1746
1805
  pollInterval = DEFAULT_WATCH_SCHEDULE.viewedMs,
@@ -1763,11 +1822,11 @@ async function startGlobalServer(options = {}) {
1763
1822
  let uiHttpServer = null;
1764
1823
  let viteServer = null;
1765
1824
  if (dev) {
1766
- uiPort = await getPort();
1825
+ uiPort = await getPort({ port: preferredUiPort });
1767
1826
  const uiRoot = resolveUiRoot();
1768
1827
  viteServer = await startViteDevServer(uiRoot, uiPort, silent);
1769
1828
  } else {
1770
- uiPort = await getPort();
1829
+ uiPort = await getPort({ port: preferredUiPort });
1771
1830
  const uiDist = resolveUiDist();
1772
1831
  uiHttpServer = await createStaticServer(uiDist, uiPort);
1773
1832
  }
@@ -1913,6 +1972,10 @@ async function startGlobalServer(options = {}) {
1913
1972
  pid: process.pid,
1914
1973
  startedAt: Date.now()
1915
1974
  };
1975
+ const serverBuiltAt = builtAt();
1976
+ if (serverBuiltAt !== null) {
1977
+ serverInfo.builtAt = serverBuiltAt;
1978
+ }
1916
1979
  writeServerFile(serverInfo);
1917
1980
  if (!silent) {
1918
1981
  console.log(`
@@ -1930,9 +1993,23 @@ Waiting for reviews...
1930
1993
  if (openBrowser) {
1931
1994
  await open(uiUrl);
1932
1995
  }
1996
+ const serverStartedAt = Date.now();
1997
+ const pendingOpens = /* @__PURE__ */ new Set();
1933
1998
  reopenBrowserIfNeeded = () => {
1934
- if (!hasConnectedClients()) {
1935
- open(uiUrl);
1999
+ const openIfUnwatched = () => {
2000
+ if (reopenBrowserIfNeeded && !hasConnectedClients()) {
2001
+ open(uiUrl);
2002
+ }
2003
+ };
2004
+ const wait = serverStartedAt + reconnectGraceMs - Date.now();
2005
+ if (wait > 0) {
2006
+ const timer = setTimeout(() => {
2007
+ pendingOpens.delete(timer);
2008
+ openIfUnwatched();
2009
+ }, wait);
2010
+ pendingOpens.add(timer);
2011
+ } else {
2012
+ openIfUnwatched();
1936
2013
  }
1937
2014
  };
1938
2015
  async function stop() {
@@ -1947,6 +2024,8 @@ Waiting for reviews...
1947
2024
  }
1948
2025
  clientSessions.clear();
1949
2026
  sessions.clear();
2027
+ for (const timer of pendingOpens) clearTimeout(timer);
2028
+ pendingOpens.clear();
1950
2029
  reopenBrowserIfNeeded = null;
1951
2030
  serverUiUrl = null;
1952
2031
  await new Promise((resolve) => {
@@ -1993,11 +2072,11 @@ function mcpToolPermission(name) {
1993
2072
  export {
1994
2073
  readServerFile,
1995
2074
  isServerAlive,
2075
+ getBuildInfo,
2076
+ describeVersion,
1996
2077
  DEFAULT_DIFF_REF,
1997
2078
  COMMIT_GATE_DIFF_REF,
1998
2079
  DIFF_REF_DESCRIPTION,
1999
- getBuildInfo,
2000
- describeVersion,
2001
2080
  currentVersion,
2002
2081
  recordError,
2003
2082
  readLastError,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ensureServer,
3
3
  submitReviewToServer
4
- } from "./chunk-F6O5YP3I.js";
4
+ } from "./chunk-35YVBTBH.js";
5
5
  import {
6
6
  parseDiff
7
7
  } from "./chunk-3GMPE2ZR.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  demo
3
- } from "./chunk-FONA4U6B.js";
4
- import "./chunk-F6O5YP3I.js";
3
+ } from "./chunk-PJYDCLBP.js";
4
+ import "./chunk-35YVBTBH.js";
5
5
  import "./chunk-3GMPE2ZR.js";
6
6
  import "./chunk-DHCVZGHE.js";
7
7
  import "./chunk-JSBRDJBE.js";
@@ -14,7 +14,7 @@ import {
14
14
  recordError,
15
15
  submitReviewToServer,
16
16
  waitForDecision
17
- } from "./chunk-F6O5YP3I.js";
17
+ } from "./chunk-35YVBTBH.js";
18
18
  import {
19
19
  getDiff
20
20
  } from "./chunk-3GMPE2ZR.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {