diffing 0.2.0 → 0.2.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.
Files changed (92) hide show
  1. package/README.md +28 -4
  2. package/dist/cli-agent-BLPX3oOP.mjs +443 -0
  3. package/dist/cli-agent-BNwY5lZE.mjs +446 -0
  4. package/dist/cli-agent-CbzXDSSF.mjs +446 -0
  5. package/dist/cli-agent-DsqbS3n7.mjs +443 -0
  6. package/dist/cli-agent-ZAkixf4c.mjs +446 -0
  7. package/dist/cli.mjs +539 -33
  8. package/dist/client/assets/{angular-html-fdYbuxHF.js → angular-html-DsssUIMZ.js} +1 -1
  9. package/dist/client/assets/{angular-ts-D3iFqR-R.js → angular-ts-dTg-sJ77.js} +1 -1
  10. package/dist/client/assets/{apl-MnBV-iQN.js → apl-C1yUKbqz.js} +1 -1
  11. package/dist/client/assets/{astro-BrpK01E7.js → astro-BI07ha2y.js} +1 -1
  12. package/dist/client/assets/{blade-CGmyBuwD.js → blade-DkzUGffS.js} +1 -1
  13. package/dist/client/assets/{c-xQnwtG9Y.js → c-KkuqqOvW.js} +1 -1
  14. package/dist/client/assets/{cobol-ypVzOIQu.js → cobol-DRh7-jLi.js} +1 -1
  15. package/dist/client/assets/{coffee-CrtE1uj2.js → coffee-wR_ZAh9a.js} +1 -1
  16. package/dist/client/assets/{cpp-DLd4XEKA.js → cpp-OD_qyywn.js} +1 -1
  17. package/dist/client/assets/{crystal-CJYvpLt_.js → crystal-E4ahAddE.js} +1 -1
  18. package/dist/client/assets/{css-DrX2B9os.js → css-2NgE_uxz.js} +1 -1
  19. package/dist/client/assets/{edge-BsQYk9OW.js → edge-8mbscxxD.js} +1 -1
  20. package/dist/client/assets/{elixir-DU_VfGYR.js → elixir-CJv1b3Q6.js} +1 -1
  21. package/dist/client/assets/{elm-W4eOLEDv.js → elm-DUPzqZ9H.js} +1 -1
  22. package/dist/client/assets/{erb-D9FdIJ2e.js → erb-ssvEF26z.js} +1 -1
  23. package/dist/client/assets/{git-rebase-BtPCXjbS.js → git-rebase-DiWIK1Oa.js} +1 -1
  24. package/dist/client/assets/{glimmer-js-BVfUGMUb.js → glimmer-js-DrHg6AuA.js} +1 -1
  25. package/dist/client/assets/{glimmer-ts-BZEaHDZL.js → glimmer-ts-wQSGOa8A.js} +1 -1
  26. package/dist/client/assets/{glsl-CSiNDuwt.js → glsl-OY27QGY0.js} +1 -1
  27. package/dist/client/assets/{graphql-Crf08Rhb.js → graphql-CSWSqe6f.js} +1 -1
  28. package/dist/client/assets/{hack-DUiQuO20.js → hack-Bi1v624Q.js} +1 -1
  29. package/dist/client/assets/{haml-BtvVTDRG.js → haml-CX94zV9m.js} +1 -1
  30. package/dist/client/assets/{handlebars-Bu_cdZA_.js → handlebars-C1j6wnjG.js} +1 -1
  31. package/dist/client/assets/{html-C4cun3zD.js → html-BcAjMlB7.js} +1 -1
  32. package/dist/client/assets/{html-derivative-CQnD80KV.js → html-derivative-DcEpOzdx.js} +1 -1
  33. package/dist/client/assets/{http-DuOJF9Kg.js → http-BBxxSc6S.js} +1 -1
  34. package/dist/client/assets/{hurl-DgrdYsYh.js → hurl-CpKRNTz4.js} +1 -1
  35. package/dist/client/assets/{index-CZZd7iTh.js → index-Bwv7BWUp.js} +156 -84
  36. package/dist/client/assets/index-CoZyJYkw.css +1 -0
  37. package/dist/client/assets/{java-D-vtFiv8.js → java-CFUlLvAA.js} +1 -1
  38. package/dist/client/assets/{javascript-CFYFXvdN.js → javascript-C-HnT3sE.js} +1 -1
  39. package/dist/client/assets/{jinja-fZvIg2_s.js → jinja-CPU8RwNP.js} +1 -1
  40. package/dist/client/assets/{jison-DEzxLq5g.js → jison-CW0TBQOs.js} +1 -1
  41. package/dist/client/assets/{json-QM4VBqMC.js → json-qUjlNuEJ.js} +1 -1
  42. package/dist/client/assets/{jsx-DLzUM_cC.js → jsx-KWeo60kf.js} +1 -1
  43. package/dist/client/assets/{julia-Dwt1SIpn.js → julia-CnJB8y2e.js} +1 -1
  44. package/dist/client/assets/{just-Dwp-hLcL.js → just-Do22d9PP.js} +1 -1
  45. package/dist/client/assets/{latex-CMF1JViz.js → latex-CCPgzvKn.js} +1 -1
  46. package/dist/client/assets/{liquid-czkpOZWY.js → liquid-B9btdZoI.js} +1 -1
  47. package/dist/client/assets/{lua-DE5PD0Vh.js → lua-IFRifurW.js} +1 -1
  48. package/dist/client/assets/{marko-DHeEvZiW.js → marko-CCaVBFq1.js} +1 -1
  49. package/dist/client/assets/{mdc-B941liUQ.js → mdc-BtbnOLUc.js} +1 -1
  50. package/dist/client/assets/{nginx-Pb9t8c-r.js → nginx-BkbFZHVt.js} +1 -1
  51. package/dist/client/assets/{nim-MMUvW_R1.js → nim-2uCd5j65.js} +1 -1
  52. package/dist/client/assets/{perl-DXwXRmuC.js → perl-C4ixqSNT.js} +1 -1
  53. package/dist/client/assets/{php-D42kv59r.js → php-DJ8WOtcI.js} +1 -1
  54. package/dist/client/assets/{pug-v0iesski.js → pug-BOaafhT5.js} +1 -1
  55. package/dist/client/assets/{qml-spxQQ-ei.js → qml-CURE-1y4.js} +1 -1
  56. package/dist/client/assets/{r-CobWTBG9.js → r-CPWmgzo3.js} +1 -1
  57. package/dist/client/assets/{razor-B-_Ww3MB.js → razor-DH83VP_d.js} +1 -1
  58. package/dist/client/assets/{regexp-BJMVs8R0.js → regexp-BXVWi-t_.js} +1 -1
  59. package/dist/client/assets/{rst-BHcaWsY0.js → rst-Bbxxs6E2.js} +1 -1
  60. package/dist/client/assets/{ruby-BCMT6kog.js → ruby-C-TuWES4.js} +1 -1
  61. package/dist/client/assets/{sas-DHfZnd3R.js → sas-njzNYNOq.js} +1 -1
  62. package/dist/client/assets/{scss-DjDsJvDO.js → scss-D2i6rHB8.js} +1 -1
  63. package/dist/client/assets/{shellscript-B6qaIiU-.js → shellscript-D6Bxi40C.js} +1 -1
  64. package/dist/client/assets/{shellsession-ByCeir2b.js → shellsession-Defxq-I6.js} +1 -1
  65. package/dist/client/assets/{soy-C6WpnMSF.js → soy-D0GKXrtT.js} +1 -1
  66. package/dist/client/assets/{sql-BIvSNLV_.js → sql-BRW_ADkU.js} +1 -1
  67. package/dist/client/assets/{stata-PG9iZ--I.js → stata-w7w59F48.js} +1 -1
  68. package/dist/client/assets/{surrealql-CmvCzG09.js → surrealql-O-lLuqz7.js} +1 -1
  69. package/dist/client/assets/{svelte-ChumG88s.js → svelte-DN3DR5-t.js} +1 -1
  70. package/dist/client/assets/{templ-CmzuRY_a.js → templ-CJ2JFoia.js} +1 -1
  71. package/dist/client/assets/{tex-DL8t-_eJ.js → tex-CpWrf5g9.js} +1 -1
  72. package/dist/client/assets/{ts-tags-BFfE0Egm.js → ts-tags-CZ6Af6Pe.js} +1 -1
  73. package/dist/client/assets/{tsx-C1bFyHcE.js → tsx-gIUSpRih.js} +1 -1
  74. package/dist/client/assets/{twig-DkIluZEQ.js → twig-DwZINhmw.js} +1 -1
  75. package/dist/client/assets/{typescript-Gc86Bc3j.js → typescript-Dj2aMECR.js} +1 -1
  76. package/dist/client/assets/{vue-C88JmL53.js → vue-B5K0aWrP.js} +1 -1
  77. package/dist/client/assets/{vue-html-Cry4aksC.js → vue-html-DKGgrypd.js} +1 -1
  78. package/dist/client/assets/{vue-vine-zlDkA3Ui.js → vue-vine-B5iDYTCh.js} +1 -1
  79. package/dist/client/assets/{xml-NvwpIFDy.js → xml-CbTzxfc-.js} +1 -1
  80. package/dist/client/assets/{xsl-CDjXvY9w.js → xsl-Dj-Be6nx.js} +1 -1
  81. package/dist/client/assets/{yaml-Bh-LPjcH.js → yaml-rTK8uEjh.js} +1 -1
  82. package/dist/client/index.html +2 -5
  83. package/dist/mcp-CE77qfjx.mjs +254 -0
  84. package/dist/mcp-CFeE13n-.mjs +254 -0
  85. package/dist/mcp-SGCrfYGo.mjs +254 -0
  86. package/dist/server-lock-B4crz9-_.mjs +708 -0
  87. package/dist/server-lock-DpCrvL-Z.mjs +714 -0
  88. package/dist/update-check-DJuFE9w6.mjs +128 -0
  89. package/dist/update-check-DSC02MWQ.mjs +129 -0
  90. package/dist/update-check-KL-2sGxE.mjs +129 -0
  91. package/package.json +12 -14
  92. package/dist/client/assets/index-CkC-EvWo.css +0 -1
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as revertHunk, S as listRepoFiles, _ as getRepoRoot, a as extractPlanLines, b as gitAddFile, c as formatComments, d as getFileContent, f as getGitDiffAsync, g as getRepoName, h as getProjectStorageDir, i as writeServerLock, l as getBranchName, m as getMergeStatus, o as formatPlanReview, p as getHunkHistory, r as removeServerLock, s as sectionTitleForLine, u as getCustomGitDiffAsync, v as getTabSizeForFiles, w as isSafePath, x as isGitRepo, y as getUntrackedFilePathsAsync } from "./server-lock-CAdqFSfl.mjs";
2
+ import { C as revertHunk, S as listRepoFiles, T as toSafeRelativePath, _ as getRepoRoot, a as extractPlanLines, b as gitAddFile, c as formatComments, d as getFileContent, f as getGitDiffAsync, g as getRepoName, h as getProjectStorageDir, i as writeServerLock, l as getBranchName, m as getMergeStatus, o as formatPlanReview, p as getHunkHistory, r as removeServerLock, s as sectionTitleForLine, u as getCustomGitDiffAsync, v as getTabSizeForFiles, w as isSafePath, x as isGitRepo, y as getUntrackedFilePathsAsync } from "./server-lock-DpCrvL-Z.mjs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, extname, join, resolve } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
@@ -11,6 +11,7 @@ import { homedir } from "node:os";
11
11
  import { Hono } from "hono";
12
12
  import { streamSSE } from "hono/streaming";
13
13
  import { serve } from "@hono/node-server";
14
+ import { stdout } from "node:process";
14
15
  //#region src/lib/diff-options.ts
15
16
  const DEFAULTS$1 = {
16
17
  revisions: [],
@@ -1112,7 +1113,10 @@ const DEFAULTS = {
1112
1113
  expandContextByDefault: false,
1113
1114
  collapsedContextThreshold: 10,
1114
1115
  expansionLineCount: 20,
1115
- haptics: true
1116
+ haptics: true,
1117
+ sounds: true,
1118
+ uiFont: null,
1119
+ monoFont: null
1116
1120
  };
1117
1121
  function loadSettings() {
1118
1122
  try {
@@ -1139,9 +1143,15 @@ function saveSettings(settings) {
1139
1143
  var FileCommentStore = class {
1140
1144
  dirPath;
1141
1145
  filePath;
1142
- constructor(customRepoRoot) {
1143
- if (customRepoRoot) this.dirPath = join(customRepoRoot, ".diffing");
1144
- else this.dirPath = getProjectStorageDir();
1146
+ /**
1147
+ * @param storageDir Absolute directory to persist `comments.json` in.
1148
+ * Defaults to the per-repo storage dir under `~/.diffing` — comments are
1149
+ * NEVER written inside the reviewed (consumer) repo, so a consumer project
1150
+ * stays free of any diffing-specific artifacts. The override exists only so
1151
+ * tests can point at a throwaway temp dir.
1152
+ */
1153
+ constructor(storageDir) {
1154
+ this.dirPath = storageDir ?? getProjectStorageDir();
1145
1155
  this.filePath = join(this.dirPath, "comments.json");
1146
1156
  }
1147
1157
  async getAll() {
@@ -1261,9 +1271,15 @@ function applyUpsert(plans, input, now) {
1261
1271
  var FilePlanStore = class {
1262
1272
  dirPath;
1263
1273
  filePath;
1264
- constructor(customRepoRoot) {
1265
- if (customRepoRoot) this.dirPath = join(customRepoRoot, ".diffing");
1266
- else this.dirPath = getProjectStorageDir();
1274
+ /**
1275
+ * @param storageDir Absolute directory to persist `plans.json` in. Defaults
1276
+ * to the per-repo storage dir under `~/.diffing`, the same place comments
1277
+ * and media live — plans are NEVER written inside the reviewed (consumer)
1278
+ * repo, so a consumer project stays free of any diffing-specific artifacts.
1279
+ * The override exists only so tests can point at a throwaway temp dir.
1280
+ */
1281
+ constructor(storageDir) {
1282
+ this.dirPath = storageDir ?? getProjectStorageDir();
1267
1283
  this.filePath = join(this.dirPath, "plans.json");
1268
1284
  }
1269
1285
  async getAll() {
@@ -1389,6 +1405,39 @@ var FilePlanStore = class {
1389
1405
  }
1390
1406
  };
1391
1407
  //#endregion
1408
+ //#region src/lib/state.ts
1409
+ var FileUiStateStore = class {
1410
+ dirPath;
1411
+ filePath;
1412
+ constructor(storageDir) {
1413
+ this.dirPath = storageDir ?? getProjectStorageDir();
1414
+ this.filePath = join(this.dirPath, "ui-state.json");
1415
+ }
1416
+ async getAll() {
1417
+ try {
1418
+ const data = await readFile(this.filePath, "utf-8");
1419
+ return JSON.parse(data);
1420
+ } catch {
1421
+ return {};
1422
+ }
1423
+ }
1424
+ async setAll(state) {
1425
+ await this.save(state);
1426
+ }
1427
+ async save(state) {
1428
+ try {
1429
+ await mkdir(this.dirPath, { recursive: true });
1430
+ try {
1431
+ const repoRoot = getRepoRoot();
1432
+ await writeFile(join(this.dirPath, "repo_path.txt"), repoRoot, "utf-8");
1433
+ } catch {}
1434
+ await writeFile(this.filePath, JSON.stringify(state, null, 2), "utf-8");
1435
+ } catch (err) {
1436
+ console.error("Failed to save UI state to file:", err);
1437
+ }
1438
+ }
1439
+ };
1440
+ //#endregion
1392
1441
  //#region src/lib/review-session.ts
1393
1442
  var ReviewSession = class {
1394
1443
  round = 0;
@@ -1452,7 +1501,8 @@ var ReviewSession = class {
1452
1501
  sentAt: input.sentAt,
1453
1502
  commentXml: input.commentXml,
1454
1503
  openCount: input.openCount,
1455
- comments: input.comments
1504
+ comments: input.comments,
1505
+ decision: input.decision
1456
1506
  };
1457
1507
  this.lastPayload = payload;
1458
1508
  const waiters = [...this.waiters];
@@ -1604,6 +1654,7 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1604
1654
  const customMode = isCustomMode(diffOpts);
1605
1655
  const store = commentStore ?? new FileCommentStore();
1606
1656
  const plans = planStore ?? new FilePlanStore();
1657
+ const uiStateStore = new FileUiStateStore();
1607
1658
  const viewedFiles = /* @__PURE__ */ new Set();
1608
1659
  const activeClients = /* @__PURE__ */ new Set();
1609
1660
  const broadcast = (event, data) => {
@@ -1737,8 +1788,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1737
1788
  if (!filePath) return c.json({ error: "filePath is required" }, 400);
1738
1789
  try {
1739
1790
  const root = getRepoRoot();
1740
- if (!isSafePath(filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1741
- const absolutePath = resolve(root, filePath);
1791
+ const relPath = toSafeRelativePath(filePath, root);
1792
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1793
+ const absolutePath = resolve(root, relPath);
1742
1794
  const { exec } = await import("node:child_process");
1743
1795
  if (editor === "vscode") exec(`code "${absolutePath}"`);
1744
1796
  else if (editor === "zed") exec(`zed "${absolutePath}"`);
@@ -1812,8 +1864,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1812
1864
  const { filePath, hunkIndex } = await c.req.json();
1813
1865
  if (!filePath || typeof hunkIndex !== "number") return c.json({ error: "Missing filePath or hunkIndex" }, 400);
1814
1866
  try {
1815
- if (!isSafePath(filePath, getRepoRoot())) return c.json({ error: "Forbidden file path" }, 403);
1816
- revertHunk(filePath, hunkIndex);
1867
+ const relPath = toSafeRelativePath(filePath, getRepoRoot());
1868
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1869
+ revertHunk(relPath, hunkIndex);
1817
1870
  return c.json({ ok: true });
1818
1871
  } catch (err) {
1819
1872
  const stderr = typeof err?.stderr === "string" ? err.stderr : err?.stderr instanceof Buffer ? err.stderr.toString("utf-8") : "";
@@ -1826,8 +1879,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1826
1879
  const deletionCount = Number(c.req.query("deletionCount"));
1827
1880
  if (!filePath || Number.isNaN(deletionStart) || Number.isNaN(deletionCount)) return c.json({ error: "Missing or invalid parameters" }, 400);
1828
1881
  try {
1829
- if (!isSafePath(filePath, getRepoRoot())) return c.json({ error: "Forbidden file path" }, 403);
1830
- const history = getHunkHistory(filePath, deletionStart, deletionCount, diffOpts.revisions[0] || "HEAD");
1882
+ const relPath = toSafeRelativePath(filePath, getRepoRoot());
1883
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1884
+ const history = getHunkHistory(relPath, deletionStart, deletionCount, diffOpts.revisions[0] || "HEAD");
1831
1885
  return c.json(history);
1832
1886
  } catch (err) {
1833
1887
  return c.json({ error: err.message || "Failed to fetch hunk history" }, 500);
@@ -1838,10 +1892,11 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1838
1892
  if (!filePath || typeof content !== "string") return c.json({ error: "Missing filePath or content" }, 400);
1839
1893
  try {
1840
1894
  const root = getRepoRoot();
1841
- if (!isSafePath(filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1842
- await writeFile(resolve(root, filePath), content, "utf-8");
1895
+ const relPath = toSafeRelativePath(filePath, root);
1896
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
1897
+ await writeFile(resolve(root, relPath), content, "utf-8");
1843
1898
  if (gitAdd) try {
1844
- gitAddFile(filePath);
1899
+ gitAddFile(relPath);
1845
1900
  } catch (err) {
1846
1901
  return c.json({
1847
1902
  ok: true,
@@ -1860,6 +1915,19 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1860
1915
  const settings = saveSettings(await c.req.json());
1861
1916
  return c.json(settings);
1862
1917
  });
1918
+ app.get("/api/ui-state", async (c) => {
1919
+ return c.json(await uiStateStore.getAll());
1920
+ });
1921
+ app.put("/api/ui-state", async (c) => {
1922
+ const body = await c.req.json();
1923
+ const merged = {
1924
+ ...await uiStateStore.getAll(),
1925
+ ...body
1926
+ };
1927
+ for (const key of Object.keys(body)) if (body[key] === null || body[key] === void 0) delete merged[key];
1928
+ await uiStateStore.setAll(merged);
1929
+ return c.json(merged);
1930
+ });
1863
1931
  app.get("/api/viewed", (c) => {
1864
1932
  return c.json([...viewedFiles]);
1865
1933
  });
@@ -1940,8 +2008,9 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1940
2008
  const suggestion = match[1];
1941
2009
  try {
1942
2010
  const root = getRepoRoot();
1943
- if (!isSafePath(comment.filePath, root)) return c.json({ error: "Forbidden file path" }, 403);
1944
- const absolutePath = resolve(root, comment.filePath);
2011
+ const relPath = toSafeRelativePath(comment.filePath, root);
2012
+ if (!relPath) return c.json({ error: "Forbidden file path" }, 403);
2013
+ const absolutePath = resolve(root, relPath);
1945
2014
  const lines = (await readFile(absolutePath, "utf-8")).split(/\r?\n/);
1946
2015
  const lineIdx = comment.lineNumber - 1;
1947
2016
  if (lineIdx < 0 || lineIdx >= lines.length) return c.json({ error: "Comment line number out of range" }, 400);
@@ -1961,18 +2030,21 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
1961
2030
  app.post("/api/review/send", async (c) => {
1962
2031
  const body = await c.req.json().catch(() => ({}));
1963
2032
  const generalComment = typeof body?.generalComment === "string" ? body.generalComment : void 0;
2033
+ const decision = body?.decision === "approved" || body?.decision === "changes-requested" || body?.decision === "rejected" ? body.decision : void 0;
1964
2034
  const all = await store.getAll();
1965
2035
  const openCount = all.filter((x) => x.status === "open").length;
1966
2036
  const payload = reviewSession.send({
1967
2037
  sentAt: Date.now(),
1968
- commentXml: formatComments(all, generalComment),
2038
+ commentXml: formatComments(all, generalComment, decision),
1969
2039
  openCount,
1970
- comments: all
2040
+ comments: all,
2041
+ decision
1971
2042
  });
1972
2043
  return c.json({
1973
2044
  ok: true,
1974
2045
  round: payload.round,
1975
2046
  openCount: payload.openCount,
2047
+ decision: payload.decision,
1976
2048
  waiters: reviewSession.snapshot().waiters
1977
2049
  });
1978
2050
  });
@@ -2182,6 +2254,40 @@ function createApp(clientDir, diffOpts = DEFAULTS$1, commentStore, planStore) {
2182
2254
  });
2183
2255
  return app;
2184
2256
  }
2257
+ /**
2258
+ * Newest mtime (epoch ms) across everything that counts as project activity:
2259
+ * comments.json, plans.json, and any attachment (media) file. Plans and media
2260
+ * extend a project's life exactly like comments do, so retention is uniform
2261
+ * across all three. Returns null when none of them exist yet.
2262
+ */
2263
+ async function newestActivityMs(projectDir) {
2264
+ const candidates = [];
2265
+ const safeStat = async (p) => {
2266
+ try {
2267
+ return (await stat(p)).mtimeMs;
2268
+ } catch {
2269
+ return null;
2270
+ }
2271
+ };
2272
+ for (const name of ["comments.json", "plans.json"]) {
2273
+ const file = join(projectDir, name);
2274
+ if (!existsSync(file)) continue;
2275
+ const m = await safeStat(file);
2276
+ if (m !== null) candidates.push(m);
2277
+ }
2278
+ const attachmentsDir = join(projectDir, "attachments");
2279
+ if (existsSync(attachmentsDir)) {
2280
+ const dirM = await safeStat(attachmentsDir);
2281
+ if (dirM !== null) candidates.push(dirM);
2282
+ try {
2283
+ for (const file of await readdir(attachmentsDir)) {
2284
+ const m = await safeStat(join(attachmentsDir, file));
2285
+ if (m !== null) candidates.push(m);
2286
+ }
2287
+ } catch {}
2288
+ }
2289
+ return candidates.length ? Math.max(...candidates) : null;
2290
+ }
2185
2291
  async function cleanupStaleProjects() {
2186
2292
  const baseDir = join(homedir(), ".diffing");
2187
2293
  if (!existsSync(baseDir)) return;
@@ -2192,15 +2298,15 @@ async function cleanupStaleProjects() {
2192
2298
  for (const entry of entries) if (entry.isDirectory()) {
2193
2299
  const projectDir = join(baseDir, entry.name);
2194
2300
  const repoPathFile = join(projectDir, "repo_path.txt");
2195
- const commentsFile = join(projectDir, "comments.json");
2196
2301
  let shouldDelete = false;
2197
2302
  if (existsSync(repoPathFile)) try {
2198
2303
  const repoPath = (await readFile(repoPathFile, "utf-8")).trim();
2199
2304
  if (!repoPath || !existsSync(repoPath)) shouldDelete = true;
2200
2305
  } catch {}
2201
- if (!shouldDelete && existsSync(commentsFile)) try {
2202
- if (now - (await stat(commentsFile)).mtimeMs > STALE_TIME) shouldDelete = true;
2203
- } catch {}
2306
+ if (!shouldDelete) {
2307
+ const newest = await newestActivityMs(projectDir);
2308
+ if (newest !== null && now - newest > STALE_TIME) shouldDelete = true;
2309
+ }
2204
2310
  if (shouldDelete) await rm(projectDir, {
2205
2311
  recursive: true,
2206
2312
  force: true
@@ -2226,6 +2332,389 @@ function startServer(options) {
2226
2332
  });
2227
2333
  }
2228
2334
  //#endregion
2335
+ //#region src/lib/startup-display.ts
2336
+ const R = "\x1B[0m";
2337
+ const B = "\x1B[1m";
2338
+ const HIDE = "\x1B[?25l";
2339
+ const SHOW = "\x1B[?25h";
2340
+ const CLR = "\x1B[2K\r";
2341
+ const GLITCH_CHARS = "░▒▓█▄▀■□▸▹◆◇";
2342
+ const c = (n) => `\x1b[38;5;${n}m`;
2343
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2344
+ const up = (n) => `\x1b[${n}A`;
2345
+ const PALETTES = [
2346
+ {
2347
+ base: c(51),
2348
+ text: c(159),
2349
+ faint: c(30),
2350
+ glow: B + c(87),
2351
+ rain: c(37)
2352
+ },
2353
+ {
2354
+ base: c(82),
2355
+ text: c(155),
2356
+ faint: c(22),
2357
+ glow: B + c(118),
2358
+ rain: c(34)
2359
+ },
2360
+ {
2361
+ base: c(207),
2362
+ text: c(219),
2363
+ faint: c(96),
2364
+ glow: B + c(213),
2365
+ rain: c(128)
2366
+ },
2367
+ {
2368
+ base: c(220),
2369
+ text: c(229),
2370
+ faint: c(136),
2371
+ glow: B + c(226),
2372
+ rain: c(178)
2373
+ },
2374
+ {
2375
+ base: c(75),
2376
+ text: c(153),
2377
+ faint: c(25),
2378
+ glow: B + c(81),
2379
+ rain: c(39)
2380
+ },
2381
+ {
2382
+ base: c(214),
2383
+ text: c(223),
2384
+ faint: c(130),
2385
+ glow: B + c(208),
2386
+ rain: c(172)
2387
+ }
2388
+ ];
2389
+ const QUOTES = [
2390
+ {
2391
+ text: "The best code is no code at all.",
2392
+ author: "Jeff Atwood"
2393
+ },
2394
+ {
2395
+ text: "Programs must be written for people to read, and only incidentally for machines to execute.",
2396
+ author: "Abelson & Sussman"
2397
+ },
2398
+ {
2399
+ text: "Make it work, make it right, make it fast.",
2400
+ author: "Kent Beck"
2401
+ },
2402
+ {
2403
+ text: "Simplicity is the soul of efficiency.",
2404
+ author: "Austin Freeman"
2405
+ },
2406
+ {
2407
+ text: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
2408
+ author: "Martin Fowler"
2409
+ },
2410
+ {
2411
+ text: "First, solve the problem. Then, write the code.",
2412
+ author: "John Johnson"
2413
+ },
2414
+ {
2415
+ text: "Code is like humor. When you have to explain it, it's bad.",
2416
+ author: "Cory House"
2417
+ },
2418
+ {
2419
+ text: "Before software can be reusable it first has to be usable.",
2420
+ author: "Ralph Johnson"
2421
+ },
2422
+ {
2423
+ text: "Clean code always looks like it was written by someone who cares.",
2424
+ author: "Robert C. Martin"
2425
+ },
2426
+ {
2427
+ text: "The most dangerous phrase is \"we've always done it this way\".",
2428
+ author: "Grace Hopper"
2429
+ },
2430
+ {
2431
+ text: "Architecture is the decisions you wish you could get right early in a project.",
2432
+ author: "Martin Fowler"
2433
+ },
2434
+ {
2435
+ text: "The best error message is the one that never shows up.",
2436
+ author: "Thomas Fuchs"
2437
+ },
2438
+ {
2439
+ text: "It works on my machine.",
2440
+ author: "Every developer, always"
2441
+ },
2442
+ {
2443
+ text: "git blame yourself.",
2444
+ author: "The terminal"
2445
+ },
2446
+ {
2447
+ text: "There are only two hard things in CS: cache invalidation, naming things, and off-by-one errors.",
2448
+ author: "Unknown"
2449
+ },
2450
+ {
2451
+ text: "Debugging: being the detective in a crime movie where you're also the murderer.",
2452
+ author: "Unknown"
2453
+ },
2454
+ {
2455
+ text: "The code you wrote six months ago was written by an idiot.",
2456
+ author: "Unknown"
2457
+ },
2458
+ {
2459
+ text: "sudo make me a sandwich.",
2460
+ author: "xkcd"
2461
+ },
2462
+ {
2463
+ text: "I don't always test my code, but when I do, I do it in production.",
2464
+ author: "Unknown"
2465
+ },
2466
+ {
2467
+ text: "A QA engineer walks into a bar. Orders 0 beers. Orders 999999 beers. Orders NULL beers. Walks in through the window.",
2468
+ author: "Unknown"
2469
+ },
2470
+ {
2471
+ text: "To understand recursion, you must first understand recursion.",
2472
+ author: "Unknown"
2473
+ },
2474
+ {
2475
+ text: "The code review is the product.",
2476
+ author: "diffing"
2477
+ },
2478
+ {
2479
+ text: "Your abstractions are just someone else's bugs.",
2480
+ author: "Unknown"
2481
+ },
2482
+ {
2483
+ text: "Every rewrite is a confession that you didn't understand the problem the first time.",
2484
+ author: "Unknown"
2485
+ },
2486
+ {
2487
+ text: "The best PR is no PR — ship it in the design.",
2488
+ author: "Unknown"
2489
+ },
2490
+ {
2491
+ text: "You don't own your code. Your code owns you.",
2492
+ author: "Unknown"
2493
+ },
2494
+ {
2495
+ text: "A diff is a conversation. What story does yours tell?",
2496
+ author: "Unknown"
2497
+ },
2498
+ {
2499
+ text: "The senior engineer's superpower: knowing which shortcuts will haunt you.",
2500
+ author: "Unknown"
2501
+ },
2502
+ {
2503
+ text: "Production is the only real test environment.",
2504
+ author: "Site Reliability truth"
2505
+ },
2506
+ {
2507
+ text: "Tech debt is just deferred thinking.",
2508
+ author: "Unknown"
2509
+ },
2510
+ {
2511
+ text: "Comments lie. Code never does.",
2512
+ author: "Ron Jeffries"
2513
+ }
2514
+ ];
2515
+ function wrapText(text, width) {
2516
+ const words = text.split(" ");
2517
+ const lines = [];
2518
+ let cur = "";
2519
+ for (const word of words) {
2520
+ if (!cur) {
2521
+ cur = word;
2522
+ continue;
2523
+ }
2524
+ if (cur.length + 1 + word.length <= width) cur += " " + word;
2525
+ else {
2526
+ lines.push(cur);
2527
+ cur = word;
2528
+ }
2529
+ }
2530
+ if (cur) lines.push(cur);
2531
+ return lines;
2532
+ }
2533
+ function makeBox(quote, pal) {
2534
+ const termWidth = stdout.columns ?? 80;
2535
+ const maxContent = Math.min(64, termWidth - 8);
2536
+ const fullText = `"${quote.text}"`;
2537
+ const authorStr = `— ${quote.author}`;
2538
+ const textLines = wrapText(fullText, maxContent);
2539
+ return {
2540
+ textLines,
2541
+ authorStr,
2542
+ innerWidth: Math.max(...textLines.map((l) => l.length), authorStr.length, 28) + 4,
2543
+ pal
2544
+ };
2545
+ }
2546
+ function topBorder(b, borderCol = b.pal.base) {
2547
+ return `${borderCol}╭${"─".repeat(b.innerWidth)}╮${R}`;
2548
+ }
2549
+ function bottomBorder(b, borderCol = b.pal.base) {
2550
+ return `${borderCol}╰${"─".repeat(b.innerWidth)}╯${R}`;
2551
+ }
2552
+ function contentRow(b, text, borderCol = b.pal.base, textCol = b.pal.text) {
2553
+ return `${borderCol}│${R} ${textCol}${text}${R}${" ".repeat(b.innerWidth - 4 - text.length)} ${borderCol}│${R}`;
2554
+ }
2555
+ function authorRow(b, borderCol = b.pal.base) {
2556
+ const leading = " ".repeat(b.innerWidth - 4 - b.authorStr.length);
2557
+ return `${borderCol}│${R} ${b.pal.faint}${leading}${b.authorStr}${R} ${borderCol}│${R}`;
2558
+ }
2559
+ function allLines(b, borderCol = b.pal.base) {
2560
+ return [
2561
+ topBorder(b, borderCol),
2562
+ ...b.textLines.map((l) => contentRow(b, l, borderCol)),
2563
+ authorRow(b, borderCol),
2564
+ bottomBorder(b, borderCol)
2565
+ ];
2566
+ }
2567
+ function printBox(b) {
2568
+ for (const line of allLines(b)) stdout.write(line + "\n");
2569
+ }
2570
+ const animTypewriter = async (b) => {
2571
+ stdout.write(topBorder(b) + "\n");
2572
+ for (const line of b.textLines) {
2573
+ stdout.write(`${b.pal.base}│${R} `);
2574
+ for (let i = 0; i < line.length; i++) {
2575
+ const ch = line[i];
2576
+ stdout.write(`${b.pal.text}${ch}${b.pal.glow}█${R}`);
2577
+ await sleep(25);
2578
+ stdout.write("\b \b");
2579
+ }
2580
+ for (let blink = 0; blink < 2; blink++) {
2581
+ stdout.write(`${b.pal.glow}█${R}`);
2582
+ await sleep(100);
2583
+ stdout.write("\b \b");
2584
+ await sleep(100);
2585
+ }
2586
+ stdout.write(" ".repeat(b.innerWidth - 4 - line.length) + ` ${b.pal.base}│${R}\n`);
2587
+ }
2588
+ stdout.write(authorRow(b) + "\n");
2589
+ stdout.write(bottomBorder(b) + "\n");
2590
+ };
2591
+ const animWaveReveal = async (b) => {
2592
+ stdout.write(topBorder(b) + "\n");
2593
+ for (const line of b.textLines) {
2594
+ const half = Math.ceil(line.length / 2);
2595
+ stdout.write(contentRow(b, line.slice(0, half).padEnd(line.length), b.pal.base, b.pal.faint) + "\n");
2596
+ await sleep(130);
2597
+ stdout.write(up(1) + CLR + contentRow(b, line) + "\n");
2598
+ await sleep(60);
2599
+ }
2600
+ stdout.write(authorRow(b) + "\n");
2601
+ stdout.write(bottomBorder(b) + "\n");
2602
+ };
2603
+ const animSlide = async (b) => {
2604
+ const lines = allLines(b);
2605
+ for (let i = 0; i < lines.length; i++) stdout.write("\n");
2606
+ stdout.write(up(lines.length));
2607
+ for (let step = 0; step < 5; step++) {
2608
+ const pad = " ".repeat(Math.floor(12 * (1 - step / 4)));
2609
+ for (const line of lines) stdout.write(CLR + pad + line + "\n");
2610
+ if (step < 4) {
2611
+ stdout.write(up(lines.length));
2612
+ await sleep(80);
2613
+ }
2614
+ }
2615
+ };
2616
+ const animPulseBorder = async (b) => {
2617
+ const stages = [
2618
+ allLines(b, b.pal.faint),
2619
+ allLines(b, b.pal.base),
2620
+ allLines(b, b.pal.glow),
2621
+ allLines(b, b.pal.base)
2622
+ ];
2623
+ for (let s = 0; s < stages.length; s++) {
2624
+ if (s > 0) stdout.write(up(stages[0].length));
2625
+ for (const line of stages[s]) stdout.write((s > 0 ? CLR : "") + line + "\n");
2626
+ if (s < stages.length - 1) await sleep(180);
2627
+ }
2628
+ };
2629
+ const animGlitch = async (b) => {
2630
+ const noiseRates = [
2631
+ .25,
2632
+ .12,
2633
+ .04,
2634
+ 0
2635
+ ];
2636
+ const totalLines = b.textLines.length + 3;
2637
+ for (let f = 0; f < noiseRates.length; f++) {
2638
+ if (f > 0) stdout.write(up(totalLines));
2639
+ const rate = noiseRates[f];
2640
+ const clr = f > 0 ? CLR : "";
2641
+ const jitter = rate > 0 ? Math.floor(Math.random() * 3) : 0;
2642
+ const pad = " ".repeat(jitter);
2643
+ stdout.write(clr + pad + topBorder(b) + "\n");
2644
+ for (const line of b.textLines) if (rate === 0) stdout.write(clr + contentRow(b, line) + "\n");
2645
+ else {
2646
+ let inner = "";
2647
+ for (const ch of line) if (Math.random() < rate) {
2648
+ const charColor = Math.random() < .3 ? b.pal.glow : b.pal.base;
2649
+ inner += `${charColor}${GLITCH_CHARS[Math.floor(Math.random() * 12)]}${b.pal.text}`;
2650
+ } else inner += ch;
2651
+ const fill = " ".repeat(b.innerWidth - 4 - line.length);
2652
+ stdout.write(clr + pad + `${b.pal.base}│${R} ${b.pal.text}${inner}${R}${fill} ${b.pal.base}│${R}\n`);
2653
+ }
2654
+ stdout.write(clr + pad + authorRow(b) + "\n");
2655
+ stdout.write(clr + pad + bottomBorder(b) + "\n");
2656
+ if (f < noiseRates.length - 1) await sleep(120);
2657
+ }
2658
+ };
2659
+ const animMatrixRain = async (b) => {
2660
+ const width = b.innerWidth + 2;
2661
+ const RAIN_HEIGHT = 6;
2662
+ const streams = Array.from({ length: RAIN_HEIGHT }, () => Array(width).fill(" "));
2663
+ const chars = "日ハミヒーウシナモニサワツオリアホマケメエカキムユラセネスタヌヘ0123456789";
2664
+ for (let frame = 0; frame < 9; frame++) {
2665
+ for (let y = RAIN_HEIGHT - 1; y > 0; y--) streams[y] = [...streams[y - 1]];
2666
+ streams[0] = Array(width).fill(" ").map(() => {
2667
+ if (Math.random() < .25) return { char: chars[Math.floor(Math.random() * 42)] };
2668
+ return " ";
2669
+ });
2670
+ if (frame > 0) stdout.write(up(RAIN_HEIGHT));
2671
+ for (let y = 0; y < RAIN_HEIGHT; y++) {
2672
+ let line = "";
2673
+ for (let x = 0; x < width; x++) {
2674
+ const cell = streams[y][x];
2675
+ if (cell === " ") line += " ";
2676
+ else {
2677
+ const typedCell = cell;
2678
+ const color = y === 0 ? b.pal.glow : y <= 2 ? b.pal.text : y <= 4 ? b.pal.base : b.pal.faint;
2679
+ line += `${color}${typedCell.char}${R}`;
2680
+ }
2681
+ }
2682
+ stdout.write((frame > 0 ? CLR : "") + line + "\n");
2683
+ }
2684
+ await sleep(90);
2685
+ }
2686
+ stdout.write(up(RAIN_HEIGHT));
2687
+ for (let y = 0; y < RAIN_HEIGHT; y++) stdout.write(CLR + "\n");
2688
+ stdout.write(up(RAIN_HEIGHT));
2689
+ printBox(b);
2690
+ };
2691
+ const ANIMS = [
2692
+ animTypewriter,
2693
+ animWaveReveal,
2694
+ animSlide,
2695
+ animPulseBorder,
2696
+ animGlitch,
2697
+ animMatrixRain
2698
+ ];
2699
+ function playStartupDisplay() {
2700
+ if (!stdout.isTTY) return Promise.resolve();
2701
+ return (async () => {
2702
+ try {
2703
+ const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
2704
+ const pal = PALETTES[Math.floor(Math.random() * PALETTES.length)];
2705
+ const b = makeBox(quote, pal);
2706
+ const anim = ANIMS[Math.floor(Math.random() * ANIMS.length)];
2707
+ stdout.write("\n" + HIDE);
2708
+ await anim(b);
2709
+ stdout.write(SHOW);
2710
+ } catch {
2711
+ try {
2712
+ stdout.write(SHOW);
2713
+ } catch {}
2714
+ }
2715
+ })();
2716
+ }
2717
+ //#endregion
2229
2718
  //#region src/cli.ts
2230
2719
  const args = process.argv.slice(2);
2231
2720
  if (new Set([
@@ -2235,14 +2724,15 @@ if (new Set([
2235
2724
  "comments",
2236
2725
  "url",
2237
2726
  "mcp",
2238
- "plan"
2727
+ "plan",
2728
+ "update"
2239
2729
  ]).has(args[0])) {
2240
2730
  if (args[0] === "mcp") {
2241
- const { startMcpServer } = await import("./mcp-DKlThflu.mjs");
2731
+ const { startMcpServer } = await import("./mcp-CFeE13n-.mjs");
2242
2732
  await startMcpServer();
2243
2733
  await new Promise(() => {});
2244
2734
  }
2245
- const { runSubcommand } = await import("./cli-agent-Vbs-ePHv.mjs");
2735
+ const { runSubcommand } = await import("./cli-agent-BNwY5lZE.mjs");
2246
2736
  process.exit(await runSubcommand(args[0], args.slice(1)));
2247
2737
  }
2248
2738
  const opts = parseDiffOptions(args);
@@ -2265,9 +2755,19 @@ if (opts.outputMode === "terminal") {
2265
2755
  const exitCode = runTerminalDiff(opts);
2266
2756
  process.exit(exitCode);
2267
2757
  }
2758
+ const __pkgDir = dirname(fileURLToPath(import.meta.url));
2759
+ const currentVersion = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
2760
+ const updateCheckPromise = (async () => {
2761
+ try {
2762
+ const { checkForUpdates } = await import("./update-check-KL-2sGxE.mjs");
2763
+ return await checkForUpdates(currentVersion);
2764
+ } catch {
2765
+ return null;
2766
+ }
2767
+ })();
2268
2768
  const port = await getPort(opts.port ? { port: opts.port } : void 0);
2269
2769
  const host = opts.host;
2270
- const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "client");
2770
+ const clientDir = resolve(__pkgDir, "client");
2271
2771
  const { existsSync: existsSync$1 } = await import("node:fs");
2272
2772
  const { port: actualPort } = await startServer({
2273
2773
  port,
@@ -2283,18 +2783,24 @@ try {
2283
2783
  } catch {
2284
2784
  repoRoot = process.cwd();
2285
2785
  }
2286
- const __pkgDir = dirname(fileURLToPath(import.meta.url));
2287
- const version = JSON.parse(readFileSync(resolve(__pkgDir, "..", "package.json"), "utf-8")).version;
2288
2786
  writeServerLock({
2289
2787
  port: actualPort,
2290
2788
  host,
2291
2789
  pid: process.pid,
2292
2790
  repoRoot,
2293
2791
  startedAt: Date.now(),
2294
- version
2792
+ version: currentVersion
2295
2793
  });
2296
2794
  } catch {}
2297
2795
  console.log(`diffing server running at ${localUrl}`);
2796
+ await playStartupDisplay();
2797
+ try {
2798
+ const updateInfo = await updateCheckPromise;
2799
+ if (updateInfo?.hasUpdate) {
2800
+ const { printUpdateDisclaimer } = await import("./update-check-KL-2sGxE.mjs");
2801
+ printUpdateDisclaimer(currentVersion, updateInfo.latestVersion);
2802
+ }
2803
+ } catch {}
2298
2804
  if (!opts.noOpen) {
2299
2805
  const settings = loadSettings();
2300
2806
  const openUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;