@happy-nut/monacori 0.1.0 → 0.1.3
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 +44 -132
- package/assets/icon.png +0 -0
- package/assets/screenshots/diff-review.png +0 -0
- package/assets/screenshots/terminal.png +0 -0
- package/dist/app-main.js +210 -10
- package/dist/assets.d.ts +6 -0
- package/dist/assets.js +51 -0
- package/dist/build.d.ts +13 -0
- package/dist/build.js +77 -0
- package/dist/cli.d.ts +5 -33
- package/dist/cli.js +7 -3529
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +678 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/diff.d.ts +12 -0
- package/dist/diff.js +396 -0
- package/dist/git.d.ts +4 -0
- package/dist/git.js +23 -0
- package/dist/highlight.d.ts +1 -0
- package/dist/highlight.js +85 -0
- package/dist/i18n.d.ts +1 -0
- package/dist/i18n.js +256 -0
- package/dist/preload.cjs +83 -0
- package/dist/preload.d.cts +1 -0
- package/dist/render.d.ts +33 -0
- package/dist/render.js +406 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +175 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +18 -0
- package/dist/util.js +144 -0
- package/dist/viewer.client.js +3935 -0
- package/dist/viewer.css +1094 -0
- package/package.json +9 -3
- package/scripts/patch-electron-name.mjs +65 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const FLOW_DIR = ".monacori";
|
|
2
|
+
export const GITIGNORE_FILE = ".gitignore";
|
|
3
|
+
export const CONFIG_FILE = "config.json";
|
|
4
|
+
export const STATE_FILE = "state.md";
|
|
5
|
+
export const DECISIONS_FILE = "decisions.md";
|
|
6
|
+
export const AGENT_SNIPPET_FILE = "agent-snippet.md";
|
|
7
|
+
export const SOURCE_MAX_FILE_BYTES = 220_000;
|
|
8
|
+
export const SOURCE_MAX_TOTAL_BYTES = 50_000_000;
|
|
9
|
+
export const SOURCE_MAX_FILES = 20000;
|
|
10
|
+
// Raster images up to this size are embedded as base64 data URIs for inline preview.
|
|
11
|
+
export const IMAGE_MAX_BYTES = 2_000_000;
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DiffFile, ReviewFileState, SourceFile } from "./types.js";
|
|
2
|
+
export declare function readUnifiedDiff(options: {
|
|
3
|
+
base?: string;
|
|
4
|
+
staged: boolean;
|
|
5
|
+
context: number;
|
|
6
|
+
includeUntracked: boolean;
|
|
7
|
+
ignoreWhitespace?: boolean;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function parseUnifiedDiff(content: string): DiffFile[];
|
|
10
|
+
export declare function collectSourceFiles(diffFiles: DiffFile[]): SourceFile[];
|
|
11
|
+
export declare function collectReviewFileStates(diffFiles: DiffFile[], sourceFiles: SourceFile[]): ReviewFileState[];
|
|
12
|
+
export declare function collectHttpEnvironments(root: string): Record<string, Record<string, string>>;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { FLOW_DIR, IMAGE_MAX_BYTES, SOURCE_MAX_FILE_BYTES, SOURCE_MAX_FILES, SOURCE_MAX_TOTAL_BYTES } from "./constants.js";
|
|
5
|
+
import { formatBytes, hashText, isLikelyBinary, languageForPath, stripDiffPath } from "./util.js";
|
|
6
|
+
import { git } from "./git.js";
|
|
7
|
+
export function readUnifiedDiff(options) {
|
|
8
|
+
const args = ["diff", "--no-ext-diff", "--find-renames", `--unified=${options.context}`];
|
|
9
|
+
if (options.ignoreWhitespace)
|
|
10
|
+
args.push("--ignore-all-space");
|
|
11
|
+
if (options.staged) {
|
|
12
|
+
args.push("--cached");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
args.push(options.base ?? "HEAD");
|
|
16
|
+
}
|
|
17
|
+
args.push("--");
|
|
18
|
+
const result = spawnSync("git", args, {
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
maxBuffer: 1024 * 1024 * 100,
|
|
22
|
+
});
|
|
23
|
+
if (result.status !== 0) {
|
|
24
|
+
throw new Error(result.stderr || "git diff failed");
|
|
25
|
+
}
|
|
26
|
+
const chunks = [result.stdout ?? ""];
|
|
27
|
+
if (options.includeUntracked && !options.staged) {
|
|
28
|
+
chunks.push(readUntrackedDiff(options.context));
|
|
29
|
+
}
|
|
30
|
+
return chunks.filter(Boolean).join("\n");
|
|
31
|
+
}
|
|
32
|
+
function readUntrackedDiff(context) {
|
|
33
|
+
const files = git(process.cwd(), ["ls-files", "--others", "--exclude-standard"])
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.filter((line) => line && !line.startsWith(`${FLOW_DIR}/`));
|
|
37
|
+
const chunks = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const absolute = join(process.cwd(), file);
|
|
40
|
+
if (!existsSync(absolute) || !statSync(absolute).isFile()) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const size = statSync(absolute).size;
|
|
44
|
+
if (size > 500_000 || isLikelyBinary(absolute)) {
|
|
45
|
+
chunks.push([
|
|
46
|
+
`diff --git a/${file} b/${file}`,
|
|
47
|
+
"new file mode 100644",
|
|
48
|
+
`Binary files /dev/null and b/${file} differ`,
|
|
49
|
+
].join("\n"));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const content = readFileSync(absolute, "utf8");
|
|
53
|
+
const lines = content.split(/\r?\n/);
|
|
54
|
+
if (lines[lines.length - 1] === "") {
|
|
55
|
+
lines.pop();
|
|
56
|
+
}
|
|
57
|
+
const limited = context > 0 ? lines : lines;
|
|
58
|
+
chunks.push([
|
|
59
|
+
`diff --git a/${file} b/${file}`,
|
|
60
|
+
"new file mode 100644",
|
|
61
|
+
"--- /dev/null",
|
|
62
|
+
`+++ b/${file}`,
|
|
63
|
+
`@@ -0,0 +1,${limited.length} @@`,
|
|
64
|
+
...limited.map((line) => `+${line}`),
|
|
65
|
+
].join("\n"));
|
|
66
|
+
}
|
|
67
|
+
return chunks.join("\n");
|
|
68
|
+
}
|
|
69
|
+
export function parseUnifiedDiff(content) {
|
|
70
|
+
const files = [];
|
|
71
|
+
let current;
|
|
72
|
+
let hunk;
|
|
73
|
+
let oldLine = 0;
|
|
74
|
+
let newLine = 0;
|
|
75
|
+
for (const line of content.split(/\r?\n/)) {
|
|
76
|
+
if (line.startsWith("diff --git ")) {
|
|
77
|
+
const match = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
78
|
+
const oldPath = match?.[1] ?? "unknown";
|
|
79
|
+
const newPath = match?.[2] ?? oldPath;
|
|
80
|
+
current = {
|
|
81
|
+
oldPath,
|
|
82
|
+
newPath,
|
|
83
|
+
displayPath: newPath === "/dev/null" ? oldPath : newPath,
|
|
84
|
+
status: "modified",
|
|
85
|
+
binary: false,
|
|
86
|
+
hunks: [],
|
|
87
|
+
};
|
|
88
|
+
files.push(current);
|
|
89
|
+
hunk = undefined;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!current) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (line.startsWith("new file mode ")) {
|
|
96
|
+
current.status = "added";
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (line.startsWith("deleted file mode ")) {
|
|
100
|
+
current.status = "deleted";
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (line.startsWith("rename from ")) {
|
|
104
|
+
current.status = "renamed";
|
|
105
|
+
current.oldPath = line.slice("rename from ".length);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (line.startsWith("rename to ")) {
|
|
109
|
+
current.newPath = line.slice("rename to ".length);
|
|
110
|
+
current.displayPath = current.newPath;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (line.startsWith("--- ")) {
|
|
114
|
+
current.oldPath = stripDiffPath(line.slice(4));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (line.startsWith("+++ ")) {
|
|
118
|
+
current.newPath = stripDiffPath(line.slice(4));
|
|
119
|
+
current.displayPath = current.newPath === "/dev/null" ? current.oldPath : current.newPath;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
|
|
123
|
+
current.binary = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
|
|
127
|
+
if (hunkMatch) {
|
|
128
|
+
oldLine = Number(hunkMatch[1]);
|
|
129
|
+
newLine = Number(hunkMatch[3]);
|
|
130
|
+
hunk = {
|
|
131
|
+
header: line,
|
|
132
|
+
title: hunkMatch[5]?.trim() ?? "",
|
|
133
|
+
oldStart: oldLine,
|
|
134
|
+
newStart: newLine,
|
|
135
|
+
lines: [],
|
|
136
|
+
};
|
|
137
|
+
current.hunks.push(hunk);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!hunk) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (line.startsWith("+")) {
|
|
144
|
+
hunk.lines.push({ kind: "add", newLine, text: line.slice(1) });
|
|
145
|
+
newLine += 1;
|
|
146
|
+
}
|
|
147
|
+
else if (line.startsWith("-")) {
|
|
148
|
+
hunk.lines.push({ kind: "delete", oldLine, text: line.slice(1) });
|
|
149
|
+
oldLine += 1;
|
|
150
|
+
}
|
|
151
|
+
else if (line.startsWith(" ")) {
|
|
152
|
+
hunk.lines.push({ kind: "context", oldLine, newLine, text: line.slice(1) });
|
|
153
|
+
oldLine += 1;
|
|
154
|
+
newLine += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return files.filter((file) => file.binary || file.hunks.length > 0);
|
|
158
|
+
}
|
|
159
|
+
// Raster image extensions that get an inline base64 preview. SVG is intentionally excluded:
|
|
160
|
+
// it is text/markup, so it stays embedded as source (and can be syntax-highlighted / commented).
|
|
161
|
+
function imageMimeForPath(path) {
|
|
162
|
+
const dot = path.lastIndexOf(".");
|
|
163
|
+
const ext = dot >= 0 ? path.slice(dot + 1).toLowerCase() : "";
|
|
164
|
+
switch (ext) {
|
|
165
|
+
case "png": return "image/png";
|
|
166
|
+
case "jpg":
|
|
167
|
+
case "jpeg": return "image/jpeg";
|
|
168
|
+
case "gif": return "image/gif";
|
|
169
|
+
case "webp": return "image/webp";
|
|
170
|
+
case "bmp": return "image/bmp";
|
|
171
|
+
case "ico": return "image/x-icon";
|
|
172
|
+
case "avif": return "image/avif";
|
|
173
|
+
case "apng": return "image/apng";
|
|
174
|
+
default: return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Working-tree git status per path (git status --porcelain) for IntelliJ-style sidebar coloring:
|
|
178
|
+
// untracked => "new" (red), index/staged change => "staged" (green, git add'd), unstaged worktree
|
|
179
|
+
// change => "edited" (blue). "git add까지 되었으면" the index column wins, so staged > new/edited.
|
|
180
|
+
function gitStatusMap(cwd) {
|
|
181
|
+
const map = new Map();
|
|
182
|
+
let out = "";
|
|
183
|
+
try {
|
|
184
|
+
out = git(cwd, ["status", "--porcelain"]);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return map;
|
|
188
|
+
}
|
|
189
|
+
for (const line of out.split(/\r?\n/)) {
|
|
190
|
+
if (line.length < 3)
|
|
191
|
+
continue;
|
|
192
|
+
const x = line[0];
|
|
193
|
+
const y = line[1];
|
|
194
|
+
let path = line.slice(3);
|
|
195
|
+
const arrow = path.indexOf(" -> ");
|
|
196
|
+
if (arrow >= 0)
|
|
197
|
+
path = path.slice(arrow + 4); // rename: color the new path
|
|
198
|
+
if (path.startsWith('"') && path.endsWith('"'))
|
|
199
|
+
path = path.slice(1, -1);
|
|
200
|
+
let kind;
|
|
201
|
+
if (x === "?" && y === "?")
|
|
202
|
+
kind = "new";
|
|
203
|
+
else if (x !== " " && x !== "?")
|
|
204
|
+
kind = "staged";
|
|
205
|
+
else
|
|
206
|
+
kind = "edited";
|
|
207
|
+
map.set(path, kind);
|
|
208
|
+
}
|
|
209
|
+
return map;
|
|
210
|
+
}
|
|
211
|
+
export function collectSourceFiles(diffFiles) {
|
|
212
|
+
const changed = new Set(diffFiles
|
|
213
|
+
.map((file) => file.displayPath)
|
|
214
|
+
.filter((path) => path && path !== "/dev/null"));
|
|
215
|
+
const changedLinesByPath = new Map();
|
|
216
|
+
for (const file of diffFiles) {
|
|
217
|
+
if (!file.displayPath || file.displayPath === "/dev/null")
|
|
218
|
+
continue;
|
|
219
|
+
const nums = [];
|
|
220
|
+
for (const hunk of file.hunks) {
|
|
221
|
+
for (const line of hunk.lines) {
|
|
222
|
+
if (line.kind === "add" && typeof line.newLine === "number")
|
|
223
|
+
nums.push(line.newLine);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
changedLinesByPath.set(file.displayPath, nums);
|
|
227
|
+
}
|
|
228
|
+
const vcsByPath = gitStatusMap(process.cwd());
|
|
229
|
+
for (const file of diffFiles) {
|
|
230
|
+
const kind = vcsByPath.get(file.displayPath);
|
|
231
|
+
if (kind)
|
|
232
|
+
file.vcs = kind; // color the Changes list from the same status map
|
|
233
|
+
}
|
|
234
|
+
const paths = new Set();
|
|
235
|
+
const gitFiles = git(process.cwd(), ["ls-files", "--cached", "--others", "--exclude-standard"]);
|
|
236
|
+
for (const file of gitFiles.split(/\r?\n/)) {
|
|
237
|
+
const path = file.trim();
|
|
238
|
+
if (path && isSourceCandidate(path)) {
|
|
239
|
+
paths.add(path);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const path of changed) {
|
|
243
|
+
if (isSourceCandidate(path)) {
|
|
244
|
+
paths.add(path);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const sourceFiles = [];
|
|
248
|
+
let embeddedFiles = 0;
|
|
249
|
+
let embeddedBytes = 0;
|
|
250
|
+
for (const path of Array.from(paths).sort((a, b) => a.localeCompare(b))) {
|
|
251
|
+
const absolute = join(process.cwd(), path);
|
|
252
|
+
const base = {
|
|
253
|
+
path,
|
|
254
|
+
name: basename(path),
|
|
255
|
+
language: languageForPath(path),
|
|
256
|
+
content: "",
|
|
257
|
+
size: 0,
|
|
258
|
+
changed: changed.has(path),
|
|
259
|
+
embedded: false,
|
|
260
|
+
changedLines: changedLinesByPath.get(path) || [],
|
|
261
|
+
signature: "",
|
|
262
|
+
vcs: vcsByPath.get(path),
|
|
263
|
+
};
|
|
264
|
+
if (!existsSync(absolute)) {
|
|
265
|
+
const skippedReason = "file is not present in the working tree";
|
|
266
|
+
sourceFiles.push({ ...base, signature: hashText(`${path}\0missing\0${skippedReason}`), skippedReason });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const stats = statSync(absolute);
|
|
270
|
+
if (!stats.isFile()) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const imageMime = imageMimeForPath(path);
|
|
274
|
+
if (imageMime) {
|
|
275
|
+
if (stats.size <= IMAGE_MAX_BYTES) {
|
|
276
|
+
const dataUri = `data:${imageMime};base64,${readFileSync(absolute).toString("base64")}`;
|
|
277
|
+
sourceFiles.push({ ...base, size: stats.size, image: dataUri, signature: hashText(`${path}\0image\0${stats.size}`) });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const skippedReason = `image larger than ${formatBytes(IMAGE_MAX_BYTES)}`;
|
|
281
|
+
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0image-large\0${stats.size}`), skippedReason });
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (isLikelyBinary(absolute)) {
|
|
286
|
+
const skippedReason = "binary file";
|
|
287
|
+
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0binary\0${stats.size}`), skippedReason });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (stats.size > SOURCE_MAX_FILE_BYTES) {
|
|
291
|
+
const skippedReason = `larger than ${formatBytes(SOURCE_MAX_FILE_BYTES)}`;
|
|
292
|
+
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0large\0${stats.size}`), skippedReason });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (embeddedFiles >= SOURCE_MAX_FILES || embeddedBytes + stats.size > SOURCE_MAX_TOTAL_BYTES) {
|
|
296
|
+
const skippedReason = "source index budget reached";
|
|
297
|
+
sourceFiles.push({ ...base, size: stats.size, signature: hashText(`${path}\0budget\0${stats.size}`), skippedReason });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const content = readFileSync(absolute, "utf8");
|
|
301
|
+
sourceFiles.push({
|
|
302
|
+
...base,
|
|
303
|
+
content,
|
|
304
|
+
size: stats.size,
|
|
305
|
+
embedded: true,
|
|
306
|
+
signature: hashText(`${path}\0${content}`),
|
|
307
|
+
});
|
|
308
|
+
embeddedFiles += 1;
|
|
309
|
+
embeddedBytes += stats.size;
|
|
310
|
+
}
|
|
311
|
+
return sourceFiles;
|
|
312
|
+
}
|
|
313
|
+
export function collectReviewFileStates(diffFiles, sourceFiles) {
|
|
314
|
+
const states = new Map();
|
|
315
|
+
for (const file of sourceFiles) {
|
|
316
|
+
states.set(file.path, file.signature);
|
|
317
|
+
}
|
|
318
|
+
for (const file of diffFiles) {
|
|
319
|
+
const hunkText = file.hunks
|
|
320
|
+
.map((hunk) => [
|
|
321
|
+
hunk.header,
|
|
322
|
+
...hunk.lines.map((line) => `${line.kind}:${line.oldLine ?? ""}:${line.newLine ?? ""}:${line.text}`),
|
|
323
|
+
].join("\n"))
|
|
324
|
+
.join("\n---\n");
|
|
325
|
+
states.set(file.displayPath, hashText(`${file.displayPath}\0${file.status}\0${file.binary}\0${hunkText}`));
|
|
326
|
+
}
|
|
327
|
+
return Array.from(states.entries())
|
|
328
|
+
.map(([path, signature]) => ({ path, signature }))
|
|
329
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
330
|
+
}
|
|
331
|
+
// Reads IntelliJ-style HTTP Client environment files from the project root and
|
|
332
|
+
// merges them into { envName: { varName: value } }. The private file overrides
|
|
333
|
+
// the public one so secrets stay out of source control.
|
|
334
|
+
export function collectHttpEnvironments(root) {
|
|
335
|
+
const result = {};
|
|
336
|
+
for (const fileName of ["http-client.env.json", "http-client.private.env.json"]) {
|
|
337
|
+
const filePath = join(root, fileName);
|
|
338
|
+
if (!existsSync(filePath))
|
|
339
|
+
continue;
|
|
340
|
+
let parsed;
|
|
341
|
+
try {
|
|
342
|
+
parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!parsed || typeof parsed !== "object")
|
|
348
|
+
continue;
|
|
349
|
+
for (const [envName, rawVars] of Object.entries(parsed)) {
|
|
350
|
+
if (!rawVars || typeof rawVars !== "object")
|
|
351
|
+
continue;
|
|
352
|
+
const target = result[envName] ?? (result[envName] = {});
|
|
353
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
354
|
+
if (typeof value === "string")
|
|
355
|
+
target[key] = value;
|
|
356
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
357
|
+
target[key] = String(value);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
function isSourceCandidate(path) {
|
|
364
|
+
const normalized = path.replace(/\\/g, "/");
|
|
365
|
+
if (!normalized || normalized.startsWith(`${FLOW_DIR}/`)) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const blocked = [
|
|
369
|
+
".git/",
|
|
370
|
+
".omc/",
|
|
371
|
+
".claude/",
|
|
372
|
+
".playwright-mcp/",
|
|
373
|
+
"node_modules/",
|
|
374
|
+
"dist/",
|
|
375
|
+
"build/",
|
|
376
|
+
"coverage/",
|
|
377
|
+
"test-results/",
|
|
378
|
+
"release/",
|
|
379
|
+
".next/",
|
|
380
|
+
".turbo/",
|
|
381
|
+
".cache/",
|
|
382
|
+
".granite/",
|
|
383
|
+
".pytest_cache/",
|
|
384
|
+
"__pycache__/",
|
|
385
|
+
"tmp/",
|
|
386
|
+
"vendor/",
|
|
387
|
+
];
|
|
388
|
+
if (blocked.some((part) => normalized === part.slice(0, -1) || normalized.includes(`/${part}`) || normalized.startsWith(part))) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
const fileName = basename(normalized);
|
|
392
|
+
if (fileName === ".DS_Store" || fileName.endsWith(".lockb")) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
package/dist/git.d.ts
ADDED
package/dist/git.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export function isGitRepository(root) {
|
|
3
|
+
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
4
|
+
cwd: root,
|
|
5
|
+
encoding: "utf8",
|
|
6
|
+
});
|
|
7
|
+
return result.status === 0 && (result.stdout ?? "").trim() === "true";
|
|
8
|
+
}
|
|
9
|
+
export function git(root, args) {
|
|
10
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
11
|
+
if (result.status !== 0) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
return (result.stdout ?? "").trim();
|
|
15
|
+
}
|
|
16
|
+
export function readGitSnapshot(root) {
|
|
17
|
+
return {
|
|
18
|
+
branch: git(root, ["branch", "--show-current"]),
|
|
19
|
+
status: git(root, ["status", "--short"]),
|
|
20
|
+
diffStat: git(root, ["diff", "--stat"]),
|
|
21
|
+
recentCommits: git(root, ["log", "--oneline", "-5"]),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderDiff2Html(diffText: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { html as renderDiff2HtmlMarkup } from "diff2html";
|
|
2
|
+
import hljs from "highlight.js";
|
|
3
|
+
import { decodeEntities, languageForPath, stripHtmlTags } from "./util.js";
|
|
4
|
+
export function renderDiff2Html(diffText) {
|
|
5
|
+
if (diffText.trim().length === 0) {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
const markup = renderDiff2HtmlMarkup(diffText, {
|
|
9
|
+
outputFormat: "side-by-side",
|
|
10
|
+
drawFileList: false,
|
|
11
|
+
matching: "lines",
|
|
12
|
+
});
|
|
13
|
+
return highlightDiffHtml(markup);
|
|
14
|
+
}
|
|
15
|
+
function highlightDiffHtml(markup) {
|
|
16
|
+
const parts = markup.split(/(?=<div [^>]*class="d2h-file-wrapper")/);
|
|
17
|
+
if (parts.length <= 1) {
|
|
18
|
+
return markup;
|
|
19
|
+
}
|
|
20
|
+
return parts
|
|
21
|
+
.map((part) => (part.includes('class="d2h-file-wrapper"') ? highlightDiffWrapper(part) : part))
|
|
22
|
+
.join("");
|
|
23
|
+
}
|
|
24
|
+
function highlightDiffWrapper(wrapper) {
|
|
25
|
+
const nameMatch = wrapper.match(/<span class="d2h-file-name">([\s\S]*?)<\/span>/);
|
|
26
|
+
const path = nameMatch ? decodeEntities(stripHtmlTags(nameMatch[1])).trim() : "";
|
|
27
|
+
const language = hljsLanguageForPath(path);
|
|
28
|
+
if (!language) {
|
|
29
|
+
return wrapper;
|
|
30
|
+
}
|
|
31
|
+
return wrapper.replace(/(<span class="d2h-code-line-ctn">)([\s\S]*?)(<\/span>\s*<\/div>)/g, (whole, open, content, close) => {
|
|
32
|
+
const highlighted = highlightCtnSegments(content, language);
|
|
33
|
+
return highlighted === null ? whole : `${open}${highlighted}${close}`;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Apply hljs to a code-line container while preserving diff2html word-level
|
|
37
|
+
// change markup (e.g. <span class="d2h-change">...): tags are kept verbatim and
|
|
38
|
+
// only the text segments between them are syntax-highlighted.
|
|
39
|
+
function highlightCtnSegments(content, language) {
|
|
40
|
+
if (content.trim().length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (content.indexOf("<") < 0) {
|
|
44
|
+
const text = decodeEntities(content);
|
|
45
|
+
if (text.trim().length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return hljs.highlight(text, { language, ignoreIllegals: true }).value;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let changed = false;
|
|
56
|
+
const out = content.replace(/(<[^>]+>)|([^<]+)/g, (_match, tag, text) => {
|
|
57
|
+
if (tag) {
|
|
58
|
+
return tag;
|
|
59
|
+
}
|
|
60
|
+
const decoded = decodeEntities(text);
|
|
61
|
+
if (decoded.trim().length === 0) {
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
changed = true;
|
|
66
|
+
return hljs.highlight(decoded, { language, ignoreIllegals: true }).value;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return changed ? out : null;
|
|
73
|
+
}
|
|
74
|
+
function hljsLanguageForPath(path) {
|
|
75
|
+
if (!path) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
const lower = path.toLowerCase();
|
|
79
|
+
if (lower.endsWith(".kt") || lower.endsWith(".kts")) {
|
|
80
|
+
return "kotlin";
|
|
81
|
+
}
|
|
82
|
+
const base = languageForPath(path);
|
|
83
|
+
const mapped = base === "markup" ? "xml" : base === "text" ? "" : base;
|
|
84
|
+
return mapped && hljs.getLanguage(mapped) ? mapped : "";
|
|
85
|
+
}
|
package/dist/i18n.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MESSAGES: Record<string, Record<string, string>>;
|