local-diff-reviewer 4.0.0 → 4.0.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 (2) hide show
  1. package/dist/cli/start.js +76 -5
  2. package/package.json +1 -1
package/dist/cli/start.js CHANGED
@@ -8,7 +8,7 @@ import { basename as basename3, dirname as dirname3, join as join5, resolve as r
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
10
  // src/server/storage.ts
11
- import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
12
12
  import { createHash } from "node:crypto";
13
13
  import { homedir, platform } from "node:os";
14
14
  import { basename, dirname, join } from "node:path";
@@ -68,16 +68,27 @@ async function attachLegacyComments(repoRoot, diffHash2, diffFiles) {
68
68
  async function readCommentStore(repoRoot) {
69
69
  const path = commentsPath(repoRoot);
70
70
  try {
71
- return normalizeStore(JSON.parse(await readFile(path, "utf8")));
72
- } catch {
71
+ const text = await readFile(path, "utf8");
72
+ return normalizeStore(parseCommentStore(text));
73
+ } catch (error) {
74
+ if (!isMissingFileError(error)) {
75
+ const recovered = await recoverCommentStore(repoRoot, path, error);
76
+ if (recovered) return recovered;
77
+ }
73
78
  return readLegacyComments(repoRoot);
74
79
  }
75
80
  }
76
81
  async function writeComments(repoRoot, store) {
77
82
  const path = commentsPath(repoRoot);
83
+ const tempPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
78
84
  await mkdir(dirname(path), { recursive: true });
79
- await writeFile(path, `${JSON.stringify(store, null, 2)}
85
+ try {
86
+ await writeFile(tempPath, `${JSON.stringify(store, null, 2)}
80
87
  `, "utf8");
88
+ await rename(tempPath, path);
89
+ } finally {
90
+ await rm(tempPath, { force: true }).catch(() => void 0);
91
+ }
81
92
  }
82
93
  function commentsPath(repoRoot) {
83
94
  const repoName = basename(repoRoot) || "repo";
@@ -92,11 +103,71 @@ function commentLogsDir() {
92
103
  }
93
104
  async function readLegacyComments(repoRoot) {
94
105
  try {
95
- return normalizeStore(JSON.parse(await readFile(join(repoRoot, ".diff-review", "comments.json"), "utf8")));
106
+ return normalizeStore(parseCommentStore(await readFile(join(repoRoot, ".diff-review", "comments.json"), "utf8")));
96
107
  } catch {
97
108
  return { threads: [] };
98
109
  }
99
110
  }
111
+ async function recoverCommentStore(repoRoot, path, cause) {
112
+ try {
113
+ const recovered = normalizeStore(parseRecoverableCommentStore(await readFile(path, "utf8")));
114
+ await writeComments(repoRoot, recovered);
115
+ return recovered;
116
+ } catch {
117
+ console.warn(`Failed to read comment store ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
118
+ return void 0;
119
+ }
120
+ }
121
+ function parseCommentStore(text) {
122
+ const parsed = JSON.parse(text);
123
+ if (!Array.isArray(parsed.threads)) return { threads: [] };
124
+ return parsed;
125
+ }
126
+ function parseRecoverableCommentStore(text) {
127
+ const end = findRootJsonObjectEnd(text);
128
+ if (end === -1) throw new Error("Comment store does not contain a complete JSON object");
129
+ return parseCommentStore(text.slice(0, end));
130
+ }
131
+ function findRootJsonObjectEnd(text) {
132
+ let depth = 0;
133
+ let inString = false;
134
+ let escaped = false;
135
+ let started = false;
136
+ for (let index = 0; index < text.length; index += 1) {
137
+ const char = text[index];
138
+ if (!started) {
139
+ if (/\s/.test(char)) continue;
140
+ if (char !== "{") return -1;
141
+ started = true;
142
+ }
143
+ if (inString) {
144
+ if (escaped) {
145
+ escaped = false;
146
+ } else if (char === "\\") {
147
+ escaped = true;
148
+ } else if (char === '"') {
149
+ inString = false;
150
+ }
151
+ continue;
152
+ }
153
+ if (char === '"') {
154
+ inString = true;
155
+ continue;
156
+ }
157
+ if (char === "{") {
158
+ depth += 1;
159
+ continue;
160
+ }
161
+ if (char === "}") {
162
+ depth -= 1;
163
+ if (depth === 0) return index + 1;
164
+ }
165
+ }
166
+ return -1;
167
+ }
168
+ function isMissingFileError(error) {
169
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
170
+ }
100
171
  function normalizeStore(store) {
101
172
  const groups = /* @__PURE__ */ new Map();
102
173
  for (const thread of store.threads) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-diff-reviewer",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "private": false,
5
5
  "description": "Open a local GitHub-style diff review Web UI for the current repository.",
6
6
  "repository": {