diffprism 0.34.1 → 0.36.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/dist/bin.js CHANGED
@@ -3,17 +3,24 @@ import {
3
3
  createGitHubClient,
4
4
  fetchPullRequest,
5
5
  fetchPullRequestDiff,
6
- isServerAlive,
7
6
  normalizePr,
8
7
  parsePrRef,
9
- readServerFile,
10
- readWatchFile,
11
8
  resolveGitHubToken,
12
- startGlobalServer,
13
- startReview,
14
- startWatch,
15
9
  submitGitHubReview
16
- } from "./chunk-VASCXEMN.js";
10
+ } from "./chunk-OR6PCPZX.js";
11
+ import {
12
+ demo
13
+ } from "./chunk-UYZ3A2PB.js";
14
+ import {
15
+ ensureServer,
16
+ isServerAlive,
17
+ readServerFile,
18
+ startGlobalServer,
19
+ submitReviewToServer
20
+ } from "./chunk-ITPHDFOS.js";
21
+ import "./chunk-QGWYCEJN.js";
22
+ import "./chunk-DHCVZGHE.js";
23
+ import "./chunk-JSBRDJBE.js";
17
24
 
18
25
  // cli/src/index.ts
19
26
  import { Command } from "commander";
@@ -31,11 +38,12 @@ async function review(ref, flags) {
31
38
  diffRef = "working-copy";
32
39
  }
33
40
  try {
34
- const result = await startReview({
35
- diffRef,
41
+ const serverInfo = await ensureServer({ dev: flags.dev });
42
+ console.log("Opening review in browser...");
43
+ const { result } = await submitReviewToServer(serverInfo, diffRef, {
36
44
  title: flags.title,
37
45
  cwd: process.cwd(),
38
- dev: flags.dev
46
+ diffRef
39
47
  });
40
48
  console.log(JSON.stringify(result, null, 2));
41
49
  process.exit(0);
@@ -62,46 +70,19 @@ async function reviewPr(pr, flags) {
62
70
  console.log("PR has no changes to review.");
63
71
  return;
64
72
  }
65
- const { payload, diffSet, briefing, metadata } = normalizePr(rawDiff, prMetadata, {
73
+ const { payload, diffSet } = normalizePr(rawDiff, prMetadata, {
66
74
  title: flags.title,
67
75
  reasoning: flags.reasoning
68
76
  });
69
77
  console.log(
70
78
  `${diffSet.files.length} files, +${diffSet.files.reduce((s, f) => s + f.additions, 0)} -${diffSet.files.reduce((s, f) => s + f.deletions, 0)}`
71
79
  );
72
- let result;
73
- const serverInfo = await isServerAlive();
74
- if (serverInfo) {
75
- const createResponse = await fetch(
76
- `http://localhost:${serverInfo.httpPort}/api/reviews`,
77
- {
78
- method: "POST",
79
- headers: { "Content-Type": "application/json" },
80
- body: JSON.stringify({
81
- payload,
82
- projectPath: `github:${owner}/${repo}`,
83
- diffRef: `PR #${number}`
84
- })
85
- }
86
- );
87
- if (!createResponse.ok) {
88
- throw new Error(`Global server returned ${createResponse.status}`);
89
- }
90
- const { sessionId } = await createResponse.json();
91
- console.log(`Review session created: ${sessionId}`);
92
- console.log("Waiting for review submission...");
93
- result = await pollForResult(serverInfo.httpPort, sessionId);
94
- } else {
95
- result = await startReview({
96
- diffRef: `PR #${number}`,
97
- title: metadata.title,
98
- description: metadata.description,
99
- reasoning: metadata.reasoning,
100
- cwd: process.cwd(),
101
- dev: flags.dev,
102
- injectedPayload: payload
103
- });
104
- }
80
+ const serverInfo = await ensureServer({ dev: flags.dev });
81
+ const { result } = await submitReviewToServer(serverInfo, `PR #${number}`, {
82
+ injectedPayload: payload,
83
+ projectPath: `github:${owner}/${repo}`,
84
+ diffRef: `PR #${number}`
85
+ });
105
86
  console.log(JSON.stringify(result, null, 2));
106
87
  if (flags.postToGithub || result.decision !== "dismissed" && await promptPostToGithub()) {
107
88
  console.log("Posting review to GitHub...");
@@ -117,24 +98,6 @@ async function reviewPr(pr, flags) {
117
98
  process.exit(1);
118
99
  }
119
100
  }
120
- async function pollForResult(httpPort, sessionId) {
121
- const pollIntervalMs = 2e3;
122
- const maxWaitMs = 600 * 1e3;
123
- const start2 = Date.now();
124
- while (Date.now() - start2 < maxWaitMs) {
125
- const response = await fetch(
126
- `http://localhost:${httpPort}/api/reviews/${sessionId}/result`
127
- );
128
- if (response.ok) {
129
- const data = await response.json();
130
- if (data.result) {
131
- return data.result;
132
- }
133
- }
134
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
135
- }
136
- throw new Error("Review timed out waiting for submission.");
137
- }
138
101
  async function promptPostToGithub() {
139
102
  if (!process.stdin.isTTY) {
140
103
  return false;
@@ -169,130 +132,30 @@ name: review
169
132
  description: Open current code changes in DiffPrism's browser-based review UI for human review.
170
133
  ---
171
134
 
172
- # DiffPrism Review Skill
173
-
174
- When the user invokes \`/review\`, open the current code changes in DiffPrism for browser-based human review.
175
-
176
- ## Steps
177
-
178
- ### 1. Check for Watch Mode
179
-
180
- Before opening a new review, check if \`diffprism watch\` is already running. Look for \`.diffprism/watch.json\` at the git root. If it exists and the process is alive:
181
-
182
- - **Do NOT call \`open_review\`** (the browser is already open with live-updating diffs)
183
- - Instead, call \`mcp__diffprism__update_review_context\` to push your reasoning to the existing watch session
184
- - Then **immediately** call \`mcp__diffprism__get_review_result\` with \`wait: true\` to block until the developer submits their review
185
- - Tell the user: "DiffPrism watch is running \u2014 pushed reasoning to the live review. Waiting for your feedback..."
186
- - When the result comes back, handle it per step 5 below
187
- - Skip steps 2-4
188
-
189
- ### 2. Load Configuration
190
-
191
- Look for \`diffprism.config.json\` at the project root. If it exists, read it for preferences. If it doesn't exist, use defaults silently \u2014 do not prompt or create the file.
192
-
193
- \`\`\`json
194
- {
195
- "defaultDiffScope": "staged | unstaged | working-copy",
196
- "includeReasoning": true | false
197
- }
198
- \`\`\`
199
-
200
- **Defaults** (when fields are missing or file doesn't exist):
201
- - \`defaultDiffScope\`: \`"working-copy"\`
202
- - \`includeReasoning\`: \`true\`
203
-
204
- ### 3. Open the Review
205
-
206
- Call \`mcp__diffprism__open_review\` with:
207
-
208
- - \`diff_ref\`: Use the \`defaultDiffScope\` from config. If the user specified a scope in their message (e.g., "/review staged"), use that instead.
209
- - \`title\`: A short summary of the changes (generate from git status or the user's message).
210
- - \`description\`: A brief description of what changed and why.
211
- - \`reasoning\`: If \`includeReasoning\` is \`true\`, include your reasoning about the implementation decisions.
212
-
213
- ### 4. Handle the Result
135
+ # DiffPrism Review
214
136
 
215
- The tool blocks until the user submits their review in the browser. When it returns:
137
+ When the user invokes \`/review\`, call \`mcp__diffprism__open_review\` with:
216
138
 
217
- - **\`approved\`** \u2014 Acknowledge and proceed with whatever task was in progress.
218
- - **\`approved_with_comments\`** \u2014 Note the comments, address any actionable feedback.
219
- - **\`changes_requested\`** \u2014 Read the comments carefully, make the requested changes, and offer to open another review.
139
+ - \`diff_ref\`: \`"working-copy"\` (or what the user specified, e.g. \`"staged"\`)
140
+ - \`title\`: Brief summary of the changes
141
+ - \`reasoning\`: Your reasoning about the implementation decisions
220
142
 
221
- #### Post-Review Actions
143
+ The tool blocks until the human submits their review. Handle the result:
222
144
 
223
- The result may include a \`postReviewAction\` field. If present, **execute the action immediately without asking for confirmation** \u2014 the user already chose this action in the review UI:
145
+ - **\`approved\`** \u2014 Proceed with the task.
146
+ - **\`changes_requested\`** \u2014 Read comments, make fixes, offer to re-review.
147
+ - If \`postReviewAction\` is \`"commit"\` \u2014 commit the changes.
148
+ - If \`postReviewAction\` is \`"commit_and_pr"\` \u2014 commit and open a PR.
224
149
 
225
- - **\`"commit"\`** \u2014 Commit the reviewed changes (stage relevant files, create a commit with an appropriate message).
226
- - **\`"commit_and_pr"\`** \u2014 Commit the changes and open a pull request.
150
+ ## Headless Tools
227
151
 
228
- If \`postReviewAction\` is not present or is empty, do nothing extra \u2014 just report the result.
152
+ - \`mcp__diffprism__analyze_diff\` \u2014 Returns analysis JSON (patterns, complexity, test gaps) without opening a browser. Use proactively to self-check before requesting review.
153
+ - \`mcp__diffprism__get_diff\` \u2014 Returns structured diff JSON.
229
154
 
230
- ### 5. Error Handling
155
+ ## Rules
231
156
 
232
- If the \`mcp__diffprism__open_review\` tool is not available:
233
- - Tell the user: "The DiffPrism MCP server isn't configured. Run \`npx diffprism setup\` to set it up, then restart Claude Code."
234
-
235
- ## Global Server Mode
236
-
237
- When a global DiffPrism server is running (\`diffprism server\`), the MCP tools automatically detect it and route reviews there instead of opening a new browser tab each time. The review appears in the server's multi-session UI at the existing browser tab.
238
-
239
- This is transparent \u2014 the same \`open_review\`, \`update_review_context\`, and \`get_review_result\` tools work the same way. No changes to the workflow are needed.
240
-
241
- ## Watch Mode: Waiting for Review Feedback
242
-
243
- When \`diffprism watch\` is active (detected via \`.diffprism/watch.json\`), the developer can submit reviews at any time in the browser.
244
-
245
- **After pushing context to a watch session**, call \`mcp__diffprism__get_review_result\` with \`wait: true\` to block until the developer submits their review. This polls the result file every 2 seconds and returns as soon as feedback is available (up to 5 minutes by default).
246
-
247
- Use this pattern:
248
- 1. Push context via \`update_review_context\`
249
- 2. Call \`get_review_result\` with \`wait: true\` \u2014 this blocks until the developer submits
250
- 3. Handle the result (approved, changes_requested, etc.)
251
- 4. If changes were requested, make fixes, push updated context, and call \`get_review_result\` with \`wait: true\` again
252
-
253
- You can also check for feedback without blocking by calling \`get_review_result\` without \`wait\` at natural breakpoints (between tasks, before committing, etc.).
254
-
255
- ## Self-Review: Headless Analysis Tools
256
-
257
- DiffPrism provides two headless tools that return analysis data as JSON without opening a browser. Use these to check your own work before requesting human review.
258
-
259
- ### Available Headless Tools
260
-
261
- - **\`mcp__diffprism__get_diff\`** \u2014 Returns a structured \`DiffSet\` (files, hunks, additions/deletions) for a given diff ref. Use this to inspect exactly what changed.
262
- - **\`mcp__diffprism__analyze_diff\`** \u2014 Returns a \`ReviewBriefing\` with summary, file triage, impact detection, complexity scores, test coverage gaps, and pattern flags (security issues, TODOs, console.logs left in, etc.).
263
-
264
- Both accept a \`diff_ref\` parameter: \`"staged"\`, \`"unstaged"\`, \`"working-copy"\`, or a git range like \`"HEAD~3..HEAD"\`.
265
-
266
- ### Self-Review Loop
267
-
268
- When you've finished writing code and before requesting human review, use this pattern:
269
-
270
- 1. **Analyze your changes:** Call \`mcp__diffprism__analyze_diff\` with \`diff_ref: "working-copy"\`
271
- 2. **Check the briefing for issues:**
272
- - \`patterns\` \u2014 Look for console.logs, TODOs, security flags, disabled tests
273
- - \`testCoverage\` \u2014 Check if changed source files have corresponding test changes
274
- - \`complexity\` \u2014 Review high-complexity scores
275
- - \`impact.newDependencies\` \u2014 Verify any new deps are intentional
276
- - \`impact.breakingChanges\` \u2014 Confirm breaking changes are expected
277
- 3. **Fix any issues found** \u2014 Remove debug statements, add missing tests, address security flags
278
- 4. **Re-analyze** \u2014 Run \`analyze_diff\` again to confirm the issues are resolved
279
- 5. **Open for human review** \u2014 Once clean, use \`/review\` or \`open_review\` for final human sign-off
280
-
281
- This loop catches common issues (leftover console.logs, missing tests, security anti-patterns) before the human reviewer sees them, making reviews faster and more focused.
282
-
283
- ### When to Use Headless Tools
284
-
285
- - **After completing a coding task** \u2014 Self-check before requesting review
286
- - **During implementation** \u2014 Periodically check for patterns and issues as you work
287
- - **Before committing** \u2014 Quick sanity check on what's about to be committed
288
- - **Do NOT use these as a replacement for human review** \u2014 They complement, not replace, \`/review\`
289
-
290
- ## Behavior Rules
291
-
292
- - **IMPORTANT: Do NOT open reviews automatically.** Only open a review when the user explicitly invokes \`/review\` or directly asks for a review.
293
- - Do NOT open reviews before commits, after code changes, or at any other time unless the user requests it.
294
- - Headless tools (\`get_diff\`, \`analyze_diff\`) can be used proactively during development without explicit user request \u2014 they don't open a browser or interrupt the user.
295
- - Power users can create \`diffprism.config.json\` manually to customize defaults (diff scope, reasoning).
157
+ - Only open a review when the user explicitly asks (\`/review\` or "review my changes").
158
+ - Headless tools can be used proactively without user request.
296
159
  `;
297
160
 
298
161
  // cli/src/commands/setup.ts
@@ -494,7 +357,84 @@ async function setupGitignore(gitRoot) {
494
357
  fs.writeFileSync(filePath, GITIGNORE_ENTRIES.map((e) => e + "\n").join(""));
495
358
  return { action: "created", filePath };
496
359
  }
360
+ async function promptChoice(question, options) {
361
+ const rl = readline2.createInterface({
362
+ input: process.stdin,
363
+ output: process.stdout
364
+ });
365
+ for (let i = 0; i < options.length; i++) {
366
+ console.log(` ${i + 1}. ${options[i]}`);
367
+ }
368
+ return new Promise((resolve) => {
369
+ rl.question(question, (answer) => {
370
+ rl.close();
371
+ const num = parseInt(answer.trim(), 10);
372
+ if (num >= 1 && num <= options.length) {
373
+ resolve(num - 1);
374
+ } else {
375
+ resolve(0);
376
+ }
377
+ });
378
+ });
379
+ }
497
380
  async function setup(flags) {
381
+ const force = flags.force ?? false;
382
+ const global = flags.global ?? false;
383
+ const quiet = flags.quiet ?? false;
384
+ const isInteractive = !global && !force && !quiet && process.stdin.isTTY;
385
+ if (isInteractive) {
386
+ return setupInteractive(flags);
387
+ }
388
+ return setupBatch(flags);
389
+ }
390
+ async function setupInteractive(flags) {
391
+ const dev = flags.dev;
392
+ const gitRoot = findGitRoot(process.cwd());
393
+ console.log("\n Welcome to DiffPrism");
394
+ console.log(" Browser-based code review for agent-generated changes.\n");
395
+ const modeChoice = await promptChoice("\nHow will you use DiffPrism? ", [
396
+ "With Claude Code (recommended)",
397
+ "From the CLI only"
398
+ ]);
399
+ if (modeChoice === 0) {
400
+ if (!gitRoot) {
401
+ console.log("\n Not in a git repository \u2014 configuring globally.\n");
402
+ const outcome2 = await setupBatch({ global: true, quiet: true });
403
+ console.log(" Setting up for Claude Code...");
404
+ console.log(" \u2713 Installed /review skill");
405
+ console.log(" \u2713 Added tool permissions");
406
+ console.log("\n Restart Claude Code, then type /review to start a review.\n");
407
+ await offerDemo(dev);
408
+ return outcome2;
409
+ }
410
+ console.log("\n Setting up for Claude Code...");
411
+ const outcome = await setupBatch({ quiet: true });
412
+ if (outcome.created.length > 0 || outcome.updated.length > 0) {
413
+ console.log(" \u2713 Registered MCP server (.mcp.json)");
414
+ console.log(" \u2713 Added tool permissions (.claude/settings.json)");
415
+ console.log(" \u2713 Installed /review skill");
416
+ console.log(" \u2713 Added .diffprism to .gitignore");
417
+ } else {
418
+ console.log(" \u2713 Already configured");
419
+ }
420
+ console.log("\n Restart Claude Code, then type /review to start a review.\n");
421
+ await offerDemo(dev);
422
+ return outcome;
423
+ }
424
+ console.log("\n DiffPrism is ready to use.");
425
+ console.log(" Run `diffprism review` in any git repo to review changes.\n");
426
+ await offerDemo(dev);
427
+ return { created: [], updated: [], skipped: [] };
428
+ }
429
+ async function offerDemo(dev) {
430
+ const wantsDemo = await promptUser("Try a demo review now? (Y/n) ");
431
+ if (wantsDemo) {
432
+ console.log("");
433
+ const { demo: demo2 } = await import("./demo-JH5YOKTZ.js");
434
+ await demo2({ dev });
435
+ }
436
+ }
437
+ async function setupBatch(flags) {
498
438
  const force = flags.force ?? false;
499
439
  const global = flags.global ?? false;
500
440
  const quiet = flags.quiet ?? false;
@@ -808,7 +748,7 @@ async function start(ref, flags) {
808
748
  });
809
749
  const hasChanges = outcome.created.length > 0 || outcome.updated.length > 0;
810
750
  if (hasChanges) {
811
- console.log("\u2713 DiffPrism configured for Claude Code.");
751
+ console.log("DiffPrism configured for Claude Code.");
812
752
  }
813
753
  let diffRef;
814
754
  if (flags.staged) {
@@ -820,30 +760,23 @@ async function start(ref, flags) {
820
760
  } else {
821
761
  diffRef = "working-copy";
822
762
  }
823
- const pollInterval = flags.interval ? parseInt(flags.interval, 10) : 1e3;
824
763
  try {
825
- const handle = await startWatch({
826
- diffRef,
827
- title: flags.title,
828
- cwd: process.cwd(),
829
- dev: flags.dev,
830
- pollInterval
831
- });
832
- console.log("Use /review in Claude Code to send changes for review.");
764
+ const serverInfo = await ensureServer({ dev: flags.dev });
765
+ console.log(
766
+ `DiffPrism server at http://localhost:${serverInfo.httpPort}`
767
+ );
833
768
  if (hasChanges) {
834
769
  console.log(
835
770
  "If this is your first time, restart Claude Code first to load the MCP server."
836
771
  );
837
772
  }
838
- const shutdown = async () => {
839
- console.log("\nStopping DiffPrism...");
840
- await handle.stop();
841
- process.exit(0);
842
- };
843
- process.on("SIGINT", shutdown);
844
- process.on("SIGTERM", shutdown);
845
- await new Promise(() => {
773
+ const { result } = await submitReviewToServer(serverInfo, diffRef, {
774
+ title: flags.title,
775
+ cwd: process.cwd(),
776
+ diffRef
846
777
  });
778
+ console.log(JSON.stringify(result, null, 2));
779
+ process.exit(0);
847
780
  } catch (err) {
848
781
  const message = err instanceof Error ? err.message : String(err);
849
782
  console.error(`Error: ${message}`);
@@ -863,24 +796,19 @@ async function watch(ref, flags) {
863
796
  } else {
864
797
  diffRef = "working-copy";
865
798
  }
866
- const pollInterval = flags.interval ? parseInt(flags.interval, 10) : 1e3;
867
799
  try {
868
- const handle = await startWatch({
869
- diffRef,
800
+ const serverInfo = await ensureServer({ dev: flags.dev });
801
+ console.log(
802
+ `DiffPrism server at http://localhost:${serverInfo.httpPort}`
803
+ );
804
+ console.log("Submitting review session...");
805
+ const { result } = await submitReviewToServer(serverInfo, diffRef, {
870
806
  title: flags.title,
871
807
  cwd: process.cwd(),
872
- dev: flags.dev,
873
- pollInterval
874
- });
875
- const shutdown = async () => {
876
- console.log("\nStopping watch...");
877
- await handle.stop();
878
- process.exit(0);
879
- };
880
- process.on("SIGINT", shutdown);
881
- process.on("SIGTERM", shutdown);
882
- await new Promise(() => {
808
+ diffRef
883
809
  });
810
+ console.log(JSON.stringify(result, null, 2));
811
+ process.exit(0);
884
812
  } catch (err) {
885
813
  const message = err instanceof Error ? err.message : String(err);
886
814
  console.error(`Error: ${message}`);
@@ -891,15 +819,15 @@ async function watch(ref, flags) {
891
819
  // cli/src/commands/notify-stop.ts
892
820
  async function notifyStop() {
893
821
  try {
894
- const watchInfo = readWatchFile();
895
- if (!watchInfo) {
822
+ const serverInfo = await isServerAlive();
823
+ if (!serverInfo) {
896
824
  process.exit(0);
897
825
  return;
898
826
  }
899
827
  const controller = new AbortController();
900
828
  const timeout = setTimeout(() => controller.abort(), 2e3);
901
829
  try {
902
- await fetch(`http://localhost:${watchInfo.wsPort}/api/refresh`, {
830
+ await fetch(`http://localhost:${serverInfo.httpPort}/api/refresh`, {
903
831
  method: "POST",
904
832
  signal: controller.signal
905
833
  });
@@ -912,18 +840,33 @@ async function notifyStop() {
912
840
  }
913
841
 
914
842
  // cli/src/commands/server.ts
843
+ import { spawn } from "child_process";
844
+ import fs3 from "fs";
845
+ import path3 from "path";
846
+ import os3 from "os";
915
847
  async function server(flags) {
916
- const existing = await isServerAlive();
917
- if (existing) {
918
- console.log(`DiffPrism server is already running on port ${existing.httpPort} (PID ${existing.pid})`);
919
- console.log(`Use 'diffprism server stop' to stop it first.`);
920
- process.exit(1);
848
+ if (flags.background) {
849
+ await spawnDaemon(flags);
921
850
  return;
922
851
  }
852
+ const isDaemon = !!flags._daemon;
853
+ if (!isDaemon) {
854
+ const existing = await isServerAlive();
855
+ if (existing) {
856
+ console.log(`DiffPrism server is already running on port ${existing.httpPort} (PID ${existing.pid})`);
857
+ console.log(`Use 'diffprism server stop' to stop it first.`);
858
+ process.exit(1);
859
+ return;
860
+ }
861
+ }
923
862
  if (!isGlobalSetupDone()) {
924
- console.log("Running global setup...\n");
925
- await setup({ global: true, quiet: false });
926
- console.log("");
863
+ if (!isDaemon) {
864
+ console.log("Running global setup...\n");
865
+ }
866
+ await setup({ global: true, quiet: isDaemon });
867
+ if (!isDaemon) {
868
+ console.log("");
869
+ }
927
870
  }
928
871
  const httpPort = flags.port ? parseInt(flags.port, 10) : void 0;
929
872
  const wsPort = flags.wsPort ? parseInt(flags.wsPort, 10) : void 0;
@@ -931,10 +874,14 @@ async function server(flags) {
931
874
  const handle = await startGlobalServer({
932
875
  httpPort,
933
876
  wsPort,
934
- dev: flags.dev
877
+ dev: flags.dev,
878
+ silent: isDaemon,
879
+ openBrowser: !isDaemon
935
880
  });
936
881
  const shutdown = async () => {
937
- console.log("\nStopping server...");
882
+ if (!isDaemon) {
883
+ console.log("\nStopping server...");
884
+ }
938
885
  await handle.stop();
939
886
  process.exit(0);
940
887
  };
@@ -944,10 +891,48 @@ async function server(flags) {
944
891
  });
945
892
  } catch (err) {
946
893
  const message = err instanceof Error ? err.message : String(err);
947
- console.error(`Error starting server: ${message}`);
894
+ if (!isDaemon) {
895
+ console.error(`Error starting server: ${message}`);
896
+ }
948
897
  process.exit(1);
949
898
  }
950
899
  }
900
+ async function spawnDaemon(flags) {
901
+ const existing = await isServerAlive();
902
+ if (existing) {
903
+ console.log(`DiffPrism server is already running on port ${existing.httpPort} (PID ${existing.pid})`);
904
+ return;
905
+ }
906
+ const args = process.argv.slice(1).filter((a) => a !== "--background");
907
+ args.push("--_daemon");
908
+ const logDir = path3.join(os3.homedir(), ".diffprism");
909
+ if (!fs3.existsSync(logDir)) {
910
+ fs3.mkdirSync(logDir, { recursive: true });
911
+ }
912
+ const logPath = path3.join(logDir, "server.log");
913
+ const logFd = fs3.openSync(logPath, "a");
914
+ const child = spawn(process.execPath, args, {
915
+ detached: true,
916
+ stdio: ["ignore", logFd, logFd],
917
+ env: { ...process.env }
918
+ });
919
+ child.unref();
920
+ fs3.closeSync(logFd);
921
+ console.log("Starting DiffPrism server in background...");
922
+ const startTime = Date.now();
923
+ const timeoutMs = 15e3;
924
+ while (Date.now() - startTime < timeoutMs) {
925
+ await new Promise((resolve) => setTimeout(resolve, 500));
926
+ const info = await isServerAlive();
927
+ if (info) {
928
+ console.log(`DiffPrism server started (PID ${info.pid}, port ${info.httpPort})`);
929
+ console.log(`Logs: ${logPath}`);
930
+ return;
931
+ }
932
+ }
933
+ console.error("Timed out waiting for server to start. Check logs at:", logPath);
934
+ process.exit(1);
935
+ }
951
936
  async function serverStatus() {
952
937
  const info = await isServerAlive();
953
938
  if (!info) {
@@ -1001,20 +986,21 @@ async function serverStop() {
1001
986
 
1002
987
  // cli/src/index.ts
1003
988
  var program = new Command();
1004
- program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.34.1" : "0.0.0-dev");
989
+ program.name("diffprism").description("Local-first code review tool for agent-generated changes").version(true ? "0.36.0" : "0.0.0-dev");
990
+ program.command("demo").description("Open a sample review to see DiffPrism in action").option("--dev", "Use Vite dev server").action(demo);
1005
991
  program.command("review [ref]").description("Open a browser-based diff review").option("--staged", "Review staged changes").option("--unstaged", "Review unstaged changes").option("-t, --title <title>", "Review title").option("--dev", "Use Vite dev server with HMR instead of static files").action(review);
1006
992
  program.command("review-pr <pr>").description("Review a GitHub pull request in DiffPrism").option("-t, --title <title>", "Override review title").option("--reasoning <text>", "Agent reasoning about the PR").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(reviewPr);
1007
993
  program.command("start [ref]").description("Set up DiffPrism and start watching for changes").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--global", "Install skill globally (~/.claude/skills/)").option("--force", "Overwrite existing configuration files").action(start);
1008
994
  program.command("watch [ref]").description("Start a persistent diff watcher with live-updating browser UI").option("--staged", "Watch staged changes").option("--unstaged", "Watch unstaged changes").option("-t, --title <title>", "Review title").option("--interval <ms>", "Poll interval in milliseconds (default: 1000)").option("--dev", "Use Vite dev server with HMR instead of static files").action(watch);
1009
995
  program.command("notify-stop").description("Signal the watch server to refresh (used by Claude Code hooks)").action(notifyStop);
1010
996
  program.command("serve").description("Start the MCP server for Claude Code integration").action(serve);
1011
- 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").action((flags) => {
997
+ 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").action((flags) => {
1012
998
  setup(flags);
1013
999
  });
1014
1000
  program.command("teardown").description("Remove DiffPrism configuration from the current project").option("--global", "Remove global configuration (skill + permissions at ~/.claude/)").option("-q, --quiet", "Suppress output").action((flags) => {
1015
1001
  teardown(flags);
1016
1002
  });
1017
- var serverCmd = program.command("server").description("Start the global DiffPrism server for multi-session reviews").option("-p, --port <port>", "HTTP API port (default: 24680)").option("--ws-port <port>", "WebSocket port (default: 24681)").option("--dev", "Use Vite dev server with HMR instead of static files").action(server);
1003
+ var serverCmd = program.command("server").description("Start the global DiffPrism server for multi-session reviews").option("-p, --port <port>", "HTTP API port (default: 24680)").option("--ws-port <port>", "WebSocket port (default: 24681)").option("--dev", "Use Vite dev server with HMR instead of static files").option("--background", "Start server as a background daemon").option("--_daemon", "Internal: run as spawned daemon (do not use directly)").action(server);
1018
1004
  serverCmd.command("status").description("Check if the global server is running and list active sessions").action(serverStatus);
1019
1005
  serverCmd.command("stop").description("Stop the running global server").action(serverStop);
1020
1006
  program.parse();