@youtyan/code-viewer 0.1.32 → 0.1.34
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 +41 -0
- package/dist/code-viewer.js +1080 -351
- package/package.json +1 -1
- package/web/app.js +5944 -5477
- package/web/index.html +28 -0
- package/web/style.css +307 -0
package/dist/code-viewer.js
CHANGED
|
@@ -1,115 +1,229 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
2
17
|
|
|
3
|
-
// web-src/server/
|
|
18
|
+
// web-src/server/annotations.ts
|
|
4
19
|
import {
|
|
5
|
-
|
|
6
|
-
constants,
|
|
7
|
-
existsSync as existsSync3,
|
|
8
|
-
lstatSync as lstatSync4,
|
|
20
|
+
existsSync,
|
|
9
21
|
mkdirSync,
|
|
10
|
-
|
|
11
|
-
readFileSync as readFileSync2,
|
|
12
|
-
realpathSync,
|
|
22
|
+
readFileSync,
|
|
13
23
|
renameSync,
|
|
14
|
-
statSync as statSync2,
|
|
15
|
-
unlinkSync,
|
|
16
|
-
watch,
|
|
17
24
|
writeFileSync
|
|
18
25
|
} from "node:fs";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function
|
|
24
|
-
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
function annotationsFilePath(root) {
|
|
28
|
+
return join(root, CODE_VIEWER_DIR, ANNOTATIONS_FILE_NAME);
|
|
29
|
+
}
|
|
30
|
+
function emptyAnnotationsState() {
|
|
31
|
+
return { version: 1, sessions: [] };
|
|
32
|
+
}
|
|
33
|
+
function makeAnnotationId(prefix) {
|
|
34
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
35
|
+
const time = Date.now().toString(36);
|
|
36
|
+
return `${prefix}-${time}${random}`;
|
|
37
|
+
}
|
|
38
|
+
function normalizeLineRange(raw) {
|
|
39
|
+
if (!raw || typeof raw !== "object")
|
|
40
|
+
return;
|
|
41
|
+
const start = raw.start;
|
|
42
|
+
const end = raw.end;
|
|
43
|
+
if (!Number.isInteger(start) || start < 1)
|
|
44
|
+
return;
|
|
45
|
+
const endValue = Number.isInteger(end) && end >= start ? end : start;
|
|
46
|
+
return { start, end: endValue };
|
|
47
|
+
}
|
|
48
|
+
function parseAnnotationLine(raw) {
|
|
49
|
+
const range = /^(\d+)-(\d+)$/.exec(raw);
|
|
50
|
+
if (range) {
|
|
51
|
+
const a = Number(range[1]);
|
|
52
|
+
const b = Number(range[2]);
|
|
53
|
+
const start = Math.min(a, b);
|
|
54
|
+
const end = Math.max(a, b);
|
|
55
|
+
return start > 0 ? { start, end } : undefined;
|
|
56
|
+
}
|
|
57
|
+
const line = Number(raw);
|
|
58
|
+
return Number.isInteger(line) && line > 0 ? { start: line, end: line } : undefined;
|
|
59
|
+
}
|
|
60
|
+
function normalizeRange(raw) {
|
|
61
|
+
const from = raw && typeof raw === "object" && typeof raw.from === "string" ? raw.from || "HEAD" : "HEAD";
|
|
62
|
+
const to = raw && typeof raw === "object" && typeof raw.to === "string" ? raw.to || "worktree" : "worktree";
|
|
63
|
+
return { from, to };
|
|
64
|
+
}
|
|
65
|
+
function normalizeEntry(raw) {
|
|
66
|
+
if (!raw || typeof raw !== "object")
|
|
25
67
|
return null;
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
68
|
+
const entry = raw;
|
|
69
|
+
if (typeof entry.id !== "string" || !entry.id)
|
|
28
70
|
return null;
|
|
29
|
-
if (
|
|
30
|
-
const code = char.charCodeAt(0);
|
|
31
|
-
return code < 32 || code === 127;
|
|
32
|
-
}))
|
|
71
|
+
if (typeof entry.path !== "string" || !entry.path)
|
|
33
72
|
return null;
|
|
34
|
-
if (
|
|
73
|
+
if (typeof entry.body !== "string" || !entry.body)
|
|
35
74
|
return null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
const normalized = {
|
|
76
|
+
id: entry.id,
|
|
77
|
+
created_at: typeof entry.created_at === "string" ? entry.created_at : "",
|
|
78
|
+
path: entry.path,
|
|
79
|
+
range: normalizeRange(entry.range),
|
|
80
|
+
body: entry.body
|
|
81
|
+
};
|
|
82
|
+
const line = normalizeLineRange(entry.line);
|
|
83
|
+
if (line)
|
|
84
|
+
normalized.line = line;
|
|
85
|
+
if (typeof entry.title === "string" && entry.title)
|
|
86
|
+
normalized.title = entry.title;
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
function normalizeSession(raw) {
|
|
90
|
+
if (!raw || typeof raw !== "object")
|
|
91
|
+
return null;
|
|
92
|
+
const session = raw;
|
|
93
|
+
if (typeof session.id !== "string" || !session.id)
|
|
94
|
+
return null;
|
|
95
|
+
const entries = Array.isArray(session.entries) ? session.entries.map(normalizeEntry).filter((entry) => entry !== null) : [];
|
|
96
|
+
return {
|
|
97
|
+
id: session.id,
|
|
98
|
+
title: typeof session.title === "string" && session.title ? session.title : "Untitled session",
|
|
99
|
+
created_at: typeof session.created_at === "string" ? session.created_at : "",
|
|
100
|
+
entries
|
|
101
|
+
};
|
|
50
102
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
103
|
+
function normalizeAnnotationsState(raw) {
|
|
104
|
+
if (!raw || typeof raw !== "object")
|
|
105
|
+
return emptyAnnotationsState();
|
|
106
|
+
const sessions = raw.sessions;
|
|
107
|
+
if (!Array.isArray(sessions))
|
|
108
|
+
return emptyAnnotationsState();
|
|
109
|
+
return {
|
|
110
|
+
version: 1,
|
|
111
|
+
sessions: sessions.map(normalizeSession).filter((session) => session !== null)
|
|
112
|
+
};
|
|
59
113
|
}
|
|
60
|
-
function
|
|
114
|
+
function loadAnnotationsState(root) {
|
|
115
|
+
const file = annotationsFilePath(root);
|
|
116
|
+
if (!existsSync(file))
|
|
117
|
+
return emptyAnnotationsState();
|
|
61
118
|
try {
|
|
62
|
-
|
|
63
|
-
const inode = "ino" in stats ? stats.ino : 0;
|
|
64
|
-
return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
|
|
119
|
+
return normalizeAnnotationsState(JSON.parse(readFileSync(file, "utf8")));
|
|
65
120
|
} catch {
|
|
66
|
-
return
|
|
67
|
-
}
|
|
121
|
+
return emptyAnnotationsState();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function saveAnnotationsState(root, state) {
|
|
125
|
+
const dir = join(root, CODE_VIEWER_DIR);
|
|
126
|
+
mkdirSync(dir, { recursive: true });
|
|
127
|
+
const file = annotationsFilePath(root);
|
|
128
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
129
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}
|
|
130
|
+
`, "utf8");
|
|
131
|
+
renameSync(tmp, file);
|
|
132
|
+
}
|
|
133
|
+
function startAnnotationSession(state, title, now, id = makeAnnotationId("s")) {
|
|
134
|
+
const session = {
|
|
135
|
+
id,
|
|
136
|
+
title: title.trim().slice(0, ANNOTATION_TITLE_MAX_CHARS) || "Untitled session",
|
|
137
|
+
created_at: now,
|
|
138
|
+
entries: []
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
state: { version: 1, sessions: [...state.sessions, session] },
|
|
142
|
+
session
|
|
143
|
+
};
|
|
68
144
|
}
|
|
69
|
-
function
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
145
|
+
function addAnnotationEntry(state, input, now, makeId = makeAnnotationId) {
|
|
146
|
+
const path = input.path.replace(/^\/+|\/+$/g, "");
|
|
147
|
+
if (!path)
|
|
148
|
+
return { ok: false, error: "path is required" };
|
|
149
|
+
const body = input.body;
|
|
150
|
+
if (!body.trim())
|
|
151
|
+
return { ok: false, error: "body is required" };
|
|
152
|
+
if (Buffer.byteLength(body, "utf8") > ANNOTATION_BODY_MAX_BYTES)
|
|
153
|
+
return { ok: false, error: "body is too large" };
|
|
154
|
+
const line = input.line ? normalizeLineRange(input.line) : undefined;
|
|
155
|
+
if (input.line && !line)
|
|
156
|
+
return { ok: false, error: "invalid line" };
|
|
157
|
+
let sessions = state.sessions;
|
|
158
|
+
let session;
|
|
159
|
+
let createdSession = false;
|
|
160
|
+
if (input.session_id) {
|
|
161
|
+
session = sessions.find((s) => s.id === input.session_id);
|
|
162
|
+
if (!session)
|
|
163
|
+
return { ok: false, error: "session not found" };
|
|
164
|
+
} else {
|
|
165
|
+
session = sessions[sessions.length - 1];
|
|
73
166
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
167
|
+
if (!session) {
|
|
168
|
+
const started = startAnnotationSession(state, input.session_title || "", now, makeId("s"));
|
|
169
|
+
sessions = started.state.sessions;
|
|
170
|
+
session = started.session;
|
|
171
|
+
createdSession = true;
|
|
77
172
|
}
|
|
78
|
-
|
|
173
|
+
const entry = {
|
|
174
|
+
id: makeId("a"),
|
|
175
|
+
created_at: now,
|
|
176
|
+
path,
|
|
177
|
+
range: normalizeRange(input.range),
|
|
178
|
+
body
|
|
179
|
+
};
|
|
180
|
+
if (line)
|
|
181
|
+
entry.line = line;
|
|
182
|
+
const title = (input.title || "").trim();
|
|
183
|
+
if (title)
|
|
184
|
+
entry.title = title.slice(0, ANNOTATION_TITLE_MAX_CHARS);
|
|
185
|
+
const updatedSession = {
|
|
186
|
+
...session,
|
|
187
|
+
entries: [...session.entries, entry]
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
state: {
|
|
192
|
+
version: 1,
|
|
193
|
+
sessions: sessions.map((s) => s.id === updatedSession.id ? updatedSession : s)
|
|
194
|
+
},
|
|
195
|
+
session: updatedSession,
|
|
196
|
+
entry,
|
|
197
|
+
created_session: createdSession
|
|
198
|
+
};
|
|
79
199
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return
|
|
200
|
+
function deleteAnnotationById(state, id) {
|
|
201
|
+
for (const session of state.sessions) {
|
|
202
|
+
if (session.id === id) {
|
|
203
|
+
return {
|
|
204
|
+
state: {
|
|
205
|
+
version: 1,
|
|
206
|
+
sessions: state.sessions.filter((s) => s.id !== id)
|
|
207
|
+
},
|
|
208
|
+
removed: "session"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (session.entries.some((entry) => entry.id === id)) {
|
|
212
|
+
return {
|
|
213
|
+
state: {
|
|
214
|
+
version: 1,
|
|
215
|
+
sessions: state.sessions.map((s) => s.id === session.id ? { ...s, entries: s.entries.filter((e) => e.id !== id) } : s)
|
|
216
|
+
},
|
|
217
|
+
removed: "entry"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { state, removed: null };
|
|
102
222
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
lstatSync as lstatSync2,
|
|
108
|
-
readdirSync,
|
|
109
|
-
readFileSync,
|
|
110
|
-
statSync
|
|
111
|
-
} from "node:fs";
|
|
112
|
-
import { join as join2 } from "node:path";
|
|
223
|
+
var CODE_VIEWER_DIR = ".code-viewer", ANNOTATIONS_FILE_NAME = "annotations.json", ANNOTATION_BODY_MAX_BYTES, ANNOTATION_TITLE_MAX_CHARS = 300;
|
|
224
|
+
var init_annotations = __esm(() => {
|
|
225
|
+
ANNOTATION_BODY_MAX_BYTES = 64 * 1024;
|
|
226
|
+
});
|
|
113
227
|
|
|
114
228
|
// web-src/server/runtime.ts
|
|
115
229
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -275,43 +389,17 @@ async function writeWebResponse(res, response) {
|
|
|
275
389
|
body.pipe(res);
|
|
276
390
|
});
|
|
277
391
|
}
|
|
392
|
+
var init_runtime = () => {};
|
|
278
393
|
|
|
279
394
|
// web-src/server/git.ts
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"venv",
|
|
289
|
-
".next",
|
|
290
|
-
".nuxt",
|
|
291
|
-
".svelte-kit",
|
|
292
|
-
".astro",
|
|
293
|
-
".vercel",
|
|
294
|
-
"dist",
|
|
295
|
-
"build",
|
|
296
|
-
"out",
|
|
297
|
-
"target",
|
|
298
|
-
".gradle",
|
|
299
|
-
".pnpm-store",
|
|
300
|
-
".turbo",
|
|
301
|
-
"__pycache__",
|
|
302
|
-
".pytest_cache",
|
|
303
|
-
".tox",
|
|
304
|
-
".terraform",
|
|
305
|
-
".idea",
|
|
306
|
-
".vscode",
|
|
307
|
-
"vendor",
|
|
308
|
-
".cache",
|
|
309
|
-
"coverage",
|
|
310
|
-
"DerivedData",
|
|
311
|
-
"Pods",
|
|
312
|
-
"bin",
|
|
313
|
-
"obj"
|
|
314
|
-
];
|
|
395
|
+
import {
|
|
396
|
+
existsSync as existsSync2,
|
|
397
|
+
lstatSync,
|
|
398
|
+
readdirSync,
|
|
399
|
+
readFileSync as readFileSync2,
|
|
400
|
+
statSync
|
|
401
|
+
} from "node:fs";
|
|
402
|
+
import { join as join2 } from "node:path";
|
|
315
403
|
function run(args, cwd) {
|
|
316
404
|
return runSync(args, cwd);
|
|
317
405
|
}
|
|
@@ -583,13 +671,18 @@ function numstatZ(args, cwd) {
|
|
|
583
671
|
}
|
|
584
672
|
return files;
|
|
585
673
|
}
|
|
674
|
+
function isToolInternalPath(path) {
|
|
675
|
+
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
|
|
676
|
+
}
|
|
586
677
|
function untracked(cwd, path = "") {
|
|
587
678
|
const args = ["git", "ls-files", "--others", "--exclude-standard"];
|
|
588
679
|
if (path)
|
|
589
680
|
args.push("--", `${path}/`);
|
|
590
681
|
const res = run(args, cwd);
|
|
591
|
-
|
|
592
|
-
|
|
682
|
+
if (res.code !== 0)
|
|
683
|
+
return [];
|
|
684
|
+
return res.stdout.split(`
|
|
685
|
+
`).filter(Boolean).filter((entry) => !isToolInternalPath(entry));
|
|
593
686
|
}
|
|
594
687
|
function normalizeTreePath(path) {
|
|
595
688
|
return path.replace(/^\/+|\/+$/g, "");
|
|
@@ -607,7 +700,7 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
|
|
|
607
700
|
return omitDirNames.has(name) ? "heavy" : undefined;
|
|
608
701
|
}
|
|
609
702
|
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
|
|
610
|
-
if (excludeNames.has(name.toLowerCase()))
|
|
703
|
+
if (excludeNames.has(name.toLowerCase()) || isToolInternalPath(name))
|
|
611
704
|
return {
|
|
612
705
|
name,
|
|
613
706
|
path: "",
|
|
@@ -669,7 +762,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
669
762
|
return;
|
|
670
763
|
}
|
|
671
764
|
for (const entry of entries) {
|
|
672
|
-
if (excludeNameSet.has(entry.name.toLowerCase()))
|
|
765
|
+
if (excludeNameSet.has(entry.name.toLowerCase()) || isToolInternalPath(entry.name))
|
|
673
766
|
continue;
|
|
674
767
|
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
675
768
|
const full = join2(dir, entry.name);
|
|
@@ -704,7 +797,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
704
797
|
}
|
|
705
798
|
function hasDotGitEntry(dir) {
|
|
706
799
|
try {
|
|
707
|
-
|
|
800
|
+
lstatSync(join2(dir, ".git"));
|
|
708
801
|
return true;
|
|
709
802
|
} catch (err) {
|
|
710
803
|
return !!err && typeof err === "object" && "code" in err && err.code !== "ENOENT";
|
|
@@ -774,12 +867,12 @@ function untrackedMeta(cwd) {
|
|
|
774
867
|
let lines = 0;
|
|
775
868
|
let fileExists = false;
|
|
776
869
|
try {
|
|
777
|
-
fileExists =
|
|
870
|
+
fileExists = existsSync2(full) && statSync(full).isFile();
|
|
778
871
|
} catch {
|
|
779
872
|
fileExists = false;
|
|
780
873
|
}
|
|
781
874
|
if (fileExists) {
|
|
782
|
-
const data =
|
|
875
|
+
const data = readFileSync2(full);
|
|
783
876
|
const probe = data.subarray(0, 8192);
|
|
784
877
|
binary = probe.includes(0);
|
|
785
878
|
if (!binary)
|
|
@@ -917,6 +1010,512 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
|
|
|
917
1010
|
lineTruncated
|
|
918
1011
|
};
|
|
919
1012
|
}
|
|
1013
|
+
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1014
|
+
var init_git = __esm(() => {
|
|
1015
|
+
init_runtime();
|
|
1016
|
+
DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
1017
|
+
"node_modules",
|
|
1018
|
+
".venv",
|
|
1019
|
+
"venv",
|
|
1020
|
+
".next",
|
|
1021
|
+
".nuxt",
|
|
1022
|
+
".svelte-kit",
|
|
1023
|
+
".astro",
|
|
1024
|
+
".vercel",
|
|
1025
|
+
"dist",
|
|
1026
|
+
"build",
|
|
1027
|
+
"out",
|
|
1028
|
+
"target",
|
|
1029
|
+
".gradle",
|
|
1030
|
+
".pnpm-store",
|
|
1031
|
+
".turbo",
|
|
1032
|
+
"__pycache__",
|
|
1033
|
+
".pytest_cache",
|
|
1034
|
+
".tox",
|
|
1035
|
+
".terraform",
|
|
1036
|
+
".idea",
|
|
1037
|
+
".vscode",
|
|
1038
|
+
"vendor",
|
|
1039
|
+
".cache",
|
|
1040
|
+
"coverage",
|
|
1041
|
+
"DerivedData",
|
|
1042
|
+
"Pods",
|
|
1043
|
+
"bin",
|
|
1044
|
+
"obj"
|
|
1045
|
+
];
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
// web-src/server/server-registry.ts
|
|
1049
|
+
import { createHash } from "node:crypto";
|
|
1050
|
+
import {
|
|
1051
|
+
existsSync as existsSync3,
|
|
1052
|
+
mkdirSync as mkdirSync2,
|
|
1053
|
+
readFileSync as readFileSync3,
|
|
1054
|
+
unlinkSync,
|
|
1055
|
+
writeFileSync as writeFileSync2
|
|
1056
|
+
} from "node:fs";
|
|
1057
|
+
import { homedir } from "node:os";
|
|
1058
|
+
import { join as join3 } from "node:path";
|
|
1059
|
+
function registryDir() {
|
|
1060
|
+
return join3(homedir(), ".cache", "code-viewer", "servers");
|
|
1061
|
+
}
|
|
1062
|
+
function serverRegistryFilePath(root) {
|
|
1063
|
+
const hash = createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
1064
|
+
return join3(registryDir(), `${hash}.json`);
|
|
1065
|
+
}
|
|
1066
|
+
function writeServerRegistry(entry) {
|
|
1067
|
+
try {
|
|
1068
|
+
mkdirSync2(registryDir(), { recursive: true });
|
|
1069
|
+
writeFileSync2(serverRegistryFilePath(entry.root), `${JSON.stringify(entry, null, 2)}
|
|
1070
|
+
`, "utf8");
|
|
1071
|
+
} catch {}
|
|
1072
|
+
}
|
|
1073
|
+
function readServerRegistry(root) {
|
|
1074
|
+
const file = serverRegistryFilePath(root);
|
|
1075
|
+
if (!existsSync3(file))
|
|
1076
|
+
return null;
|
|
1077
|
+
try {
|
|
1078
|
+
const raw = JSON.parse(readFileSync3(file, "utf8"));
|
|
1079
|
+
if (!raw || typeof raw !== "object")
|
|
1080
|
+
return null;
|
|
1081
|
+
const entry = raw;
|
|
1082
|
+
if (typeof entry.url !== "string" || !entry.url)
|
|
1083
|
+
return null;
|
|
1084
|
+
return {
|
|
1085
|
+
url: entry.url,
|
|
1086
|
+
pid: typeof entry.pid === "number" ? entry.pid : 0,
|
|
1087
|
+
root: typeof entry.root === "string" ? entry.root : root,
|
|
1088
|
+
started_at: typeof entry.started_at === "string" ? entry.started_at : ""
|
|
1089
|
+
};
|
|
1090
|
+
} catch {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function removeServerRegistry(root, pid) {
|
|
1095
|
+
try {
|
|
1096
|
+
const entry = readServerRegistry(root);
|
|
1097
|
+
if (!entry || entry.pid !== pid)
|
|
1098
|
+
return;
|
|
1099
|
+
unlinkSync(serverRegistryFilePath(root));
|
|
1100
|
+
} catch {}
|
|
1101
|
+
}
|
|
1102
|
+
var init_server_registry = () => {};
|
|
1103
|
+
|
|
1104
|
+
// web-src/server/annotate-cli.ts
|
|
1105
|
+
var exports_annotate_cli = {};
|
|
1106
|
+
__export(exports_annotate_cli, {
|
|
1107
|
+
runAnnotateCli: () => runAnnotateCli,
|
|
1108
|
+
parseAnnotateArgs: () => parseAnnotateArgs,
|
|
1109
|
+
ANNOTATE_HELP: () => ANNOTATE_HELP
|
|
1110
|
+
});
|
|
1111
|
+
import { readFileSync as readFileSync4, realpathSync } from "node:fs";
|
|
1112
|
+
function takeValue(argv, index, flag) {
|
|
1113
|
+
const value = argv[index + 1];
|
|
1114
|
+
if (value === undefined)
|
|
1115
|
+
return { error: `${flag} requires a value` };
|
|
1116
|
+
return { value, next: index + 1 };
|
|
1117
|
+
}
|
|
1118
|
+
function parseAnnotateArgs(argv) {
|
|
1119
|
+
const rest = [];
|
|
1120
|
+
let cwd;
|
|
1121
|
+
let server;
|
|
1122
|
+
const options = new Map;
|
|
1123
|
+
const flags = new Set;
|
|
1124
|
+
const valueFlags = new Set([
|
|
1125
|
+
"--title",
|
|
1126
|
+
"--file",
|
|
1127
|
+
"--line",
|
|
1128
|
+
"--from",
|
|
1129
|
+
"--to",
|
|
1130
|
+
"--session",
|
|
1131
|
+
"--session-title",
|
|
1132
|
+
"--body",
|
|
1133
|
+
"--body-file"
|
|
1134
|
+
]);
|
|
1135
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1136
|
+
const arg = argv[i];
|
|
1137
|
+
if (arg === "--help" || arg === "-h")
|
|
1138
|
+
return { ok: true, args: { command: { kind: "help" } } };
|
|
1139
|
+
if (arg === "--cwd" || arg === "--server") {
|
|
1140
|
+
const taken = takeValue(argv, i, arg);
|
|
1141
|
+
if ("error" in taken)
|
|
1142
|
+
return { ok: false, error: taken.error };
|
|
1143
|
+
if (arg === "--cwd")
|
|
1144
|
+
cwd = taken.value;
|
|
1145
|
+
else
|
|
1146
|
+
server = taken.value;
|
|
1147
|
+
i = taken.next;
|
|
1148
|
+
} else if (valueFlags.has(arg)) {
|
|
1149
|
+
const taken = takeValue(argv, i, arg);
|
|
1150
|
+
if ("error" in taken)
|
|
1151
|
+
return { ok: false, error: taken.error };
|
|
1152
|
+
options.set(arg, taken.value);
|
|
1153
|
+
i = taken.next;
|
|
1154
|
+
} else if (arg === "--json") {
|
|
1155
|
+
flags.add(arg);
|
|
1156
|
+
} else if (arg.startsWith("-")) {
|
|
1157
|
+
return { ok: false, error: `unknown option: ${arg}` };
|
|
1158
|
+
} else {
|
|
1159
|
+
rest.push(arg);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const subcommand = rest[0];
|
|
1163
|
+
if (!subcommand)
|
|
1164
|
+
return { ok: true, args: { command: { kind: "help" } } };
|
|
1165
|
+
if (subcommand === "start") {
|
|
1166
|
+
return {
|
|
1167
|
+
ok: true,
|
|
1168
|
+
args: {
|
|
1169
|
+
command: { kind: "start", title: options.get("--title") || "" },
|
|
1170
|
+
cwd,
|
|
1171
|
+
server
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
if (subcommand === "add") {
|
|
1176
|
+
const file = options.get("--file");
|
|
1177
|
+
if (!file)
|
|
1178
|
+
return { ok: false, error: "add requires --file <path>" };
|
|
1179
|
+
let line;
|
|
1180
|
+
const rawLine = options.get("--line");
|
|
1181
|
+
if (rawLine !== undefined) {
|
|
1182
|
+
line = parseAnnotationLine(rawLine);
|
|
1183
|
+
if (!line)
|
|
1184
|
+
return { ok: false, error: "--line must be <n> or <n>-<m>" };
|
|
1185
|
+
}
|
|
1186
|
+
const body = options.get("--body");
|
|
1187
|
+
const bodyFile = options.get("--body-file");
|
|
1188
|
+
if (body !== undefined && bodyFile !== undefined)
|
|
1189
|
+
return { ok: false, error: "use either --body or --body-file" };
|
|
1190
|
+
return {
|
|
1191
|
+
ok: true,
|
|
1192
|
+
args: {
|
|
1193
|
+
command: {
|
|
1194
|
+
kind: "add",
|
|
1195
|
+
file,
|
|
1196
|
+
line,
|
|
1197
|
+
from: options.get("--from"),
|
|
1198
|
+
to: options.get("--to"),
|
|
1199
|
+
title: options.get("--title"),
|
|
1200
|
+
session: options.get("--session"),
|
|
1201
|
+
sessionTitle: options.get("--session-title"),
|
|
1202
|
+
body,
|
|
1203
|
+
bodyFile
|
|
1204
|
+
},
|
|
1205
|
+
cwd,
|
|
1206
|
+
server
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (subcommand === "list") {
|
|
1211
|
+
return {
|
|
1212
|
+
ok: true,
|
|
1213
|
+
args: {
|
|
1214
|
+
command: { kind: "list", json: flags.has("--json") },
|
|
1215
|
+
cwd,
|
|
1216
|
+
server
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
if (subcommand === "delete") {
|
|
1221
|
+
const id = rest[1];
|
|
1222
|
+
if (!id)
|
|
1223
|
+
return { ok: false, error: "delete requires an id" };
|
|
1224
|
+
return { ok: true, args: { command: { kind: "delete", id }, cwd, server } };
|
|
1225
|
+
}
|
|
1226
|
+
if (subcommand === "clear") {
|
|
1227
|
+
return { ok: true, args: { command: { kind: "clear" }, cwd, server } };
|
|
1228
|
+
}
|
|
1229
|
+
return { ok: false, error: `unknown annotate command: ${subcommand}` };
|
|
1230
|
+
}
|
|
1231
|
+
async function readStdin() {
|
|
1232
|
+
if (process.stdin.isTTY)
|
|
1233
|
+
return "";
|
|
1234
|
+
const chunks = [];
|
|
1235
|
+
for await (const chunk of process.stdin) {
|
|
1236
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1237
|
+
}
|
|
1238
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1239
|
+
}
|
|
1240
|
+
function resolveRepoRoot(cwdOption) {
|
|
1241
|
+
const base = cwdOption || process.cwd();
|
|
1242
|
+
try {
|
|
1243
|
+
return repoRoot(base) || realpathSync(base);
|
|
1244
|
+
} catch {
|
|
1245
|
+
console.error(`--cwd must point to an existing directory: ${base}`);
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function serverReachable(serverUrl) {
|
|
1250
|
+
try {
|
|
1251
|
+
const res = await fetch(`${serverUrl}/_annotations`, {
|
|
1252
|
+
signal: AbortSignal.timeout(1500)
|
|
1253
|
+
});
|
|
1254
|
+
return res.ok;
|
|
1255
|
+
} catch {
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async function ensureServerUrl(root, override) {
|
|
1260
|
+
if (override) {
|
|
1261
|
+
const url = override.replace(/\/+$/, "");
|
|
1262
|
+
if (await serverReachable(url))
|
|
1263
|
+
return url;
|
|
1264
|
+
console.error(`could not reach the code-viewer server at ${url}.`);
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
const registered = readServerRegistry(root);
|
|
1268
|
+
if (registered) {
|
|
1269
|
+
const url = registered.url.replace(/\/+$/, "");
|
|
1270
|
+
if (await serverReachable(url))
|
|
1271
|
+
return url;
|
|
1272
|
+
}
|
|
1273
|
+
console.error(`no running code-viewer server for this repository.
|
|
1274
|
+
` + `Start one manually (from ${root}):
|
|
1275
|
+
` + " code-viewer");
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
async function request(serverUrl, method, body) {
|
|
1279
|
+
const url = `${serverUrl}/_annotations`;
|
|
1280
|
+
const origin = new URL(serverUrl).origin;
|
|
1281
|
+
let res;
|
|
1282
|
+
try {
|
|
1283
|
+
res = await fetch(url, {
|
|
1284
|
+
method,
|
|
1285
|
+
headers: method === "POST" ? {
|
|
1286
|
+
"Content-Type": "application/json",
|
|
1287
|
+
Origin: origin,
|
|
1288
|
+
"X-Code-Viewer-Action": "1"
|
|
1289
|
+
} : {},
|
|
1290
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1291
|
+
});
|
|
1292
|
+
} catch {
|
|
1293
|
+
console.error(`could not reach the code-viewer server at ${serverUrl}.`);
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
if (!res.ok) {
|
|
1297
|
+
console.error(`server rejected the request: ${await res.text()}`);
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
return res.json();
|
|
1301
|
+
}
|
|
1302
|
+
function formatLine(line) {
|
|
1303
|
+
if (!line)
|
|
1304
|
+
return "";
|
|
1305
|
+
return line.start === line.end ? `:${line.start}` : `:${line.start}-${line.end}`;
|
|
1306
|
+
}
|
|
1307
|
+
function printList(state) {
|
|
1308
|
+
if (!state.sessions.length) {
|
|
1309
|
+
console.log("no annotations");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
for (const session of state.sessions) {
|
|
1313
|
+
console.log(`session ${session.id} ${session.title}`);
|
|
1314
|
+
session.entries.forEach((entry, index) => {
|
|
1315
|
+
const location = `${entry.path}${formatLine(entry.line)}`;
|
|
1316
|
+
const summary = (entry.title || entry.body).split(`
|
|
1317
|
+
`)[0].slice(0, 80);
|
|
1318
|
+
console.log(` ${index + 1}. [${entry.id}] ${location} ${summary}`);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async function runAnnotateCli(argv) {
|
|
1323
|
+
const parsed = parseAnnotateArgs(argv);
|
|
1324
|
+
if (parsed.ok === false) {
|
|
1325
|
+
console.error(parsed.error);
|
|
1326
|
+
console.error('Run "code-viewer annotate --help" for usage.');
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
const { command, cwd, server } = parsed.args;
|
|
1330
|
+
if (command.kind === "help") {
|
|
1331
|
+
console.log(ANNOTATE_HELP);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const root = resolveRepoRoot(cwd);
|
|
1335
|
+
const serverUrl = await ensureServerUrl(root, server);
|
|
1336
|
+
if (command.kind === "start") {
|
|
1337
|
+
const result = await request(serverUrl, "POST", {
|
|
1338
|
+
action: "start",
|
|
1339
|
+
title: command.title
|
|
1340
|
+
});
|
|
1341
|
+
console.log(`session ${result.session.id} ${result.session.title}`);
|
|
1342
|
+
console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (command.kind === "add") {
|
|
1346
|
+
let body = command.body;
|
|
1347
|
+
if (body === undefined && command.bodyFile !== undefined) {
|
|
1348
|
+
try {
|
|
1349
|
+
body = readFileSync4(command.bodyFile, "utf8");
|
|
1350
|
+
} catch {
|
|
1351
|
+
console.error(`could not read --body-file: ${command.bodyFile}`);
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (body === undefined)
|
|
1356
|
+
body = await readStdin();
|
|
1357
|
+
if (!body.trim()) {
|
|
1358
|
+
console.error("annotation body is empty. Pass --body, --body-file, or pipe stdin.");
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
const result = await request(serverUrl, "POST", {
|
|
1362
|
+
action: "add",
|
|
1363
|
+
session_id: command.session,
|
|
1364
|
+
session_title: command.sessionTitle,
|
|
1365
|
+
path: command.file,
|
|
1366
|
+
line: command.line,
|
|
1367
|
+
range: { from: command.from, to: command.to },
|
|
1368
|
+
title: command.title,
|
|
1369
|
+
body
|
|
1370
|
+
});
|
|
1371
|
+
if (result.created_session) {
|
|
1372
|
+
console.error(`created new annotation session ${result.session_id} (${result.session_title || "Untitled session"})`);
|
|
1373
|
+
}
|
|
1374
|
+
console.log(`annotated ${result.entry.path}${formatLine(result.entry.line)} ` + `[${result.entry.id}] in session ${result.session_id} (${result.session_title || "Untitled session"})`);
|
|
1375
|
+
console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (command.kind === "list") {
|
|
1379
|
+
const state = await request(serverUrl, "GET");
|
|
1380
|
+
if (command.json)
|
|
1381
|
+
console.log(JSON.stringify(state, null, 2));
|
|
1382
|
+
else
|
|
1383
|
+
printList(state);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
if (command.kind === "delete") {
|
|
1387
|
+
const result = await request(serverUrl, "POST", {
|
|
1388
|
+
action: "delete",
|
|
1389
|
+
id: command.id
|
|
1390
|
+
});
|
|
1391
|
+
if (!result.removed) {
|
|
1392
|
+
console.error(`no annotation or session with id ${command.id}`);
|
|
1393
|
+
process.exit(1);
|
|
1394
|
+
}
|
|
1395
|
+
console.log(`deleted ${result.removed} ${command.id}`);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
await request(serverUrl, "POST", { action: "clear" });
|
|
1399
|
+
console.log("cleared all annotations");
|
|
1400
|
+
}
|
|
1401
|
+
var ANNOTATE_HELP = `code-viewer annotate — attach explanations to code locations
|
|
1402
|
+
|
|
1403
|
+
The annotations show up live in the code-viewer browser UI and are stored
|
|
1404
|
+
in <repo>/.code-viewer/annotations.json. A running code-viewer server for
|
|
1405
|
+
the repository is required: start one with "code-viewer" before using
|
|
1406
|
+
annotate (or point at one explicitly with --server).
|
|
1407
|
+
|
|
1408
|
+
Usage:
|
|
1409
|
+
code-viewer annotate start [--title <text>]
|
|
1410
|
+
code-viewer annotate add --file <path> [--line <n>|<n>-<m>]
|
|
1411
|
+
[--from <ref>] [--to <ref>] [--title <text>] [--session <id>]
|
|
1412
|
+
[--body <markdown> | --body-file <path>] (or pipe body via stdin)
|
|
1413
|
+
code-viewer annotate list [--json]
|
|
1414
|
+
code-viewer annotate delete <id>
|
|
1415
|
+
code-viewer annotate clear
|
|
1416
|
+
|
|
1417
|
+
Global options:
|
|
1418
|
+
--cwd <dir> repository to annotate (default: current directory)
|
|
1419
|
+
--server <url> code-viewer server URL (default: auto-discovered)
|
|
1420
|
+
|
|
1421
|
+
Examples:
|
|
1422
|
+
code-viewer annotate start --title "How SSE updates work"
|
|
1423
|
+
code-viewer annotate add --file web-src/server/preview.ts --line 2220-2250 \\
|
|
1424
|
+
--body "This endpoint keeps one SSE stream per browser tab."
|
|
1425
|
+
git diff HEAD~1 | code-viewer annotate add --file src/app.ts --line 10 \\
|
|
1426
|
+
--from HEAD~1 --to worktree --body "The fix moves the guard up here."
|
|
1427
|
+
`;
|
|
1428
|
+
var init_annotate_cli = __esm(() => {
|
|
1429
|
+
init_annotations();
|
|
1430
|
+
init_git();
|
|
1431
|
+
init_server_registry();
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
// web-src/directory-name.ts
|
|
1435
|
+
function normalizeNewDirectoryName(name) {
|
|
1436
|
+
if (typeof name !== "string")
|
|
1437
|
+
return null;
|
|
1438
|
+
const trimmed = name.trim();
|
|
1439
|
+
if (!trimmed || trimmed.length > 180)
|
|
1440
|
+
return null;
|
|
1441
|
+
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\x00") || Array.from(trimmed).some((char) => {
|
|
1442
|
+
const code = char.charCodeAt(0);
|
|
1443
|
+
return code < 32 || code === 127;
|
|
1444
|
+
}))
|
|
1445
|
+
return null;
|
|
1446
|
+
if (trimmed === "." || trimmed === ".." || trimmed.toLowerCase() === ".git")
|
|
1447
|
+
return null;
|
|
1448
|
+
return trimmed;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// web-src/routes.ts
|
|
1452
|
+
var SPA_PATHS, APP_ENTRY_PATHS;
|
|
1453
|
+
var init_routes = __esm(() => {
|
|
1454
|
+
SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
|
|
1455
|
+
APP_ENTRY_PATHS = ["/", "/index.html"];
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
// web-src/server/cache.ts
|
|
1459
|
+
import { lstatSync as lstatSync2 } from "node:fs";
|
|
1460
|
+
import { join as join4 } from "node:path";
|
|
1461
|
+
function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
|
|
1462
|
+
return !!cached && now - cached.storedAt <= ttlMs;
|
|
1463
|
+
}
|
|
1464
|
+
function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
|
|
1465
|
+
cache.set(key, { ...value, storedAt: now });
|
|
1466
|
+
while (cache.size > maxEntries) {
|
|
1467
|
+
const oldest = cache.keys().next().value;
|
|
1468
|
+
if (oldest === undefined)
|
|
1469
|
+
break;
|
|
1470
|
+
cache.delete(oldest);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function worktreeFileSignature(path, cwd) {
|
|
1474
|
+
try {
|
|
1475
|
+
const stats = lstatSync2(join4(cwd, path));
|
|
1476
|
+
const inode = "ino" in stats ? stats.ino : 0;
|
|
1477
|
+
return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
|
|
1478
|
+
} catch {
|
|
1479
|
+
return "state:missing";
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function fileDiffCacheKey(options) {
|
|
1483
|
+
const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
|
|
1484
|
+
if (options.isUntracked && !worktreeTarget) {
|
|
1485
|
+
throw new Error("untracked file diffs require a worktree range");
|
|
1486
|
+
}
|
|
1487
|
+
const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
|
|
1488
|
+
if (options.isUntracked) {
|
|
1489
|
+
return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
|
|
1490
|
+
}
|
|
1491
|
+
return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
|
|
1492
|
+
}
|
|
1493
|
+
var CACHE_TTL_MS = 1500, MAX_TIMED_CACHE_ENTRIES = 200;
|
|
1494
|
+
var init_cache = () => {};
|
|
1495
|
+
|
|
1496
|
+
// web-src/server/dev-assets.ts
|
|
1497
|
+
import { basename } from "node:path";
|
|
1498
|
+
function startDevAssetReload(options) {
|
|
1499
|
+
if (!options.enabled)
|
|
1500
|
+
return false;
|
|
1501
|
+
const watched = new Set(options.watchedFiles);
|
|
1502
|
+
const setTimer = options.setTimeoutFn || setTimeout;
|
|
1503
|
+
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
1504
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
1505
|
+
let timer = null;
|
|
1506
|
+
options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
|
|
1507
|
+
if (!filename || !watched.has(basename(filename.toString())))
|
|
1508
|
+
return;
|
|
1509
|
+
if (timer)
|
|
1510
|
+
clearTimer(timer);
|
|
1511
|
+
timer = setTimer(() => {
|
|
1512
|
+
timer = null;
|
|
1513
|
+
options.sendReload();
|
|
1514
|
+
}, debounceMs);
|
|
1515
|
+
});
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
var init_dev_assets = () => {};
|
|
920
1519
|
|
|
921
1520
|
// web-src/server/range.ts
|
|
922
1521
|
function isSameWorktreeRange(range) {
|
|
@@ -1130,13 +1729,13 @@ function collectLineRangeFromIndexedText(text, index, start, end) {
|
|
|
1130
1729
|
}
|
|
1131
1730
|
|
|
1132
1731
|
// web-src/server/root.ts
|
|
1133
|
-
import { existsSync as
|
|
1134
|
-
import { dirname, join as
|
|
1732
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1733
|
+
import { dirname, join as join5, normalize } from "node:path";
|
|
1135
1734
|
import { fileURLToPath } from "node:url";
|
|
1136
1735
|
function findRoot(start) {
|
|
1137
1736
|
let current = start;
|
|
1138
1737
|
for (let i = 0;i < 5; i++) {
|
|
1139
|
-
if (
|
|
1738
|
+
if (existsSync4(join5(current, "package.json")) && existsSync4(join5(current, "web"))) {
|
|
1140
1739
|
return normalize(current);
|
|
1141
1740
|
}
|
|
1142
1741
|
const parent = dirname(current);
|
|
@@ -1144,16 +1743,14 @@ function findRoot(start) {
|
|
|
1144
1743
|
break;
|
|
1145
1744
|
current = parent;
|
|
1146
1745
|
}
|
|
1147
|
-
return normalize(
|
|
1746
|
+
return normalize(join5(start, "..", ".."));
|
|
1148
1747
|
}
|
|
1149
|
-
var ROOT
|
|
1748
|
+
var ROOT;
|
|
1749
|
+
var init_root = __esm(() => {
|
|
1750
|
+
ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
|
|
1751
|
+
});
|
|
1150
1752
|
|
|
1151
1753
|
// web-src/server/search.ts
|
|
1152
|
-
var GREP_DEFAULT_MAX = 200;
|
|
1153
|
-
var GREP_ABSOLUTE_MAX = 500;
|
|
1154
|
-
var GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1155
|
-
var FILE_SEARCH_ABSOLUTE_MAX = 50000;
|
|
1156
|
-
var DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
|
|
1157
1754
|
function normalizeGrepMax(value) {
|
|
1158
1755
|
const parsed = Number(value || "");
|
|
1159
1756
|
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
@@ -1165,7 +1762,7 @@ function isSkippableSearchPath(path, omitDirNames = [], excludeNames = []) {
|
|
|
1165
1762
|
const excluded = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
1166
1763
|
return path.split(/[\\/]+/).some((part) => {
|
|
1167
1764
|
const lower = part.toLowerCase();
|
|
1168
|
-
return lower === ".git" || omitDirs.has(lower) || excluded.has(lower);
|
|
1765
|
+
return lower === ".git" || lower === ".code-viewer" || omitDirs.has(lower) || excluded.has(lower);
|
|
1169
1766
|
});
|
|
1170
1767
|
}
|
|
1171
1768
|
function fixedStringLineMatches(path, text, query, max) {
|
|
@@ -1267,6 +1864,11 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
|
|
|
1267
1864
|
`);
|
|
1268
1865
|
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1269
1866
|
}
|
|
1867
|
+
var GREP_DEFAULT_MAX = 200, GREP_ABSOLUTE_MAX = 500, GREP_MAX_FILE_BYTES, FILE_SEARCH_ABSOLUTE_MAX = 50000, DEFAULT_EXCLUDE_NAMES;
|
|
1868
|
+
var init_search = __esm(() => {
|
|
1869
|
+
GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1870
|
+
DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
|
|
1871
|
+
});
|
|
1270
1872
|
|
|
1271
1873
|
// web-src/server/worktree-watcher.ts
|
|
1272
1874
|
import {
|
|
@@ -1274,7 +1876,7 @@ import {
|
|
|
1274
1876
|
readdirSync as nodeReaddirSync,
|
|
1275
1877
|
watch as nodeWatch
|
|
1276
1878
|
} from "node:fs";
|
|
1277
|
-
import { join as
|
|
1879
|
+
import { join as join6, relative } from "node:path";
|
|
1278
1880
|
function normalizeRelativePath(path) {
|
|
1279
1881
|
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1280
1882
|
}
|
|
@@ -1357,7 +1959,7 @@ function startWorktreeUpdateWatch(options) {
|
|
|
1357
1959
|
for (const entry of entries) {
|
|
1358
1960
|
if (!entry.isDirectory())
|
|
1359
1961
|
continue;
|
|
1360
|
-
children.push(
|
|
1962
|
+
children.push(join6(dir, entry.name));
|
|
1361
1963
|
}
|
|
1362
1964
|
return children;
|
|
1363
1965
|
};
|
|
@@ -1386,10 +1988,10 @@ function startWorktreeUpdateWatch(options) {
|
|
|
1386
1988
|
scheduleUpdate();
|
|
1387
1989
|
return;
|
|
1388
1990
|
}
|
|
1389
|
-
const changed = normalizeRelativePath(
|
|
1991
|
+
const changed = normalizeRelativePath(join6(rel, filename.toString()));
|
|
1390
1992
|
if (ignored(changed))
|
|
1391
1993
|
return;
|
|
1392
|
-
const fullChangedPath =
|
|
1994
|
+
const fullChangedPath = join6(options.root, changed);
|
|
1393
1995
|
if (!isInsideRoot(options.root, fullChangedPath))
|
|
1394
1996
|
return;
|
|
1395
1997
|
const known = watchers.has(fullChangedPath);
|
|
@@ -1439,79 +2041,29 @@ function startWorktreeUpdateWatch(options) {
|
|
|
1439
2041
|
watchDirectory(options.root, true);
|
|
1440
2042
|
return { started: watchers.size > 0, close: closeAll };
|
|
1441
2043
|
}
|
|
2044
|
+
var init_worktree_watcher = __esm(() => {
|
|
2045
|
+
init_search();
|
|
2046
|
+
});
|
|
1442
2047
|
|
|
1443
2048
|
// web-src/server/preview.ts
|
|
1444
|
-
var
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
".md",
|
|
1463
|
-
".markdown",
|
|
1464
|
-
".json",
|
|
1465
|
-
".csv",
|
|
1466
|
-
".tsv",
|
|
1467
|
-
".yaml",
|
|
1468
|
-
".yml",
|
|
1469
|
-
".toml",
|
|
1470
|
-
".png",
|
|
1471
|
-
".jpg",
|
|
1472
|
-
".jpeg",
|
|
1473
|
-
".gif",
|
|
1474
|
-
".webp",
|
|
1475
|
-
".svg",
|
|
1476
|
-
".pdf",
|
|
1477
|
-
".mp4",
|
|
1478
|
-
".mov",
|
|
1479
|
-
".m4v",
|
|
1480
|
-
".webm",
|
|
1481
|
-
".mp3",
|
|
1482
|
-
".wav",
|
|
1483
|
-
".m4a",
|
|
1484
|
-
".aac",
|
|
1485
|
-
".flac",
|
|
1486
|
-
".ogg",
|
|
1487
|
-
".zip",
|
|
1488
|
-
".ts",
|
|
1489
|
-
".tsx",
|
|
1490
|
-
".js",
|
|
1491
|
-
".jsx",
|
|
1492
|
-
".css",
|
|
1493
|
-
".scss",
|
|
1494
|
-
".html"
|
|
1495
|
-
]);
|
|
1496
|
-
var generation = 1;
|
|
1497
|
-
var cwd = repoRoot(process.cwd()) || process.cwd();
|
|
1498
|
-
var cliArgs = DEFAULT_ARGS;
|
|
1499
|
-
var listenPort = 0;
|
|
1500
|
-
var openAfterStart = false;
|
|
1501
|
-
var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1502
|
-
var scopeOmitDirCliOverride = null;
|
|
1503
|
-
var scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
1504
|
-
var uploadDisabledByConfig = false;
|
|
1505
|
-
var rgAvailableCache = null;
|
|
1506
|
-
var enc = new TextEncoder;
|
|
1507
|
-
var sseClients = new Set;
|
|
1508
|
-
var fileCache = new Map;
|
|
1509
|
-
var metaCache = new Map;
|
|
1510
|
-
var fileListCache = new Map;
|
|
1511
|
-
var lineIndexCache = new Map;
|
|
1512
|
-
var blobLineIndexCache = new Map;
|
|
1513
|
-
var blobBytesCache = new Map;
|
|
1514
|
-
var blobLineCacheBytes = 0;
|
|
2049
|
+
var exports_preview = {};
|
|
2050
|
+
import {
|
|
2051
|
+
closeSync,
|
|
2052
|
+
constants,
|
|
2053
|
+
existsSync as existsSync5,
|
|
2054
|
+
lstatSync as lstatSync4,
|
|
2055
|
+
mkdirSync as mkdirSync3,
|
|
2056
|
+
openSync,
|
|
2057
|
+
readFileSync as readFileSync5,
|
|
2058
|
+
realpathSync as realpathSync2,
|
|
2059
|
+
renameSync as renameSync2,
|
|
2060
|
+
statSync as statSync2,
|
|
2061
|
+
unlinkSync as unlinkSync2,
|
|
2062
|
+
watch,
|
|
2063
|
+
writeFileSync as writeFileSync3
|
|
2064
|
+
} from "node:fs";
|
|
2065
|
+
import { homedir as homedir2 } from "node:os";
|
|
2066
|
+
import { basename as basename2, dirname as dirname2, extname, join as join7, relative as relative2 } from "node:path";
|
|
1515
2067
|
function parseCli() {
|
|
1516
2068
|
const rest = [];
|
|
1517
2069
|
for (let i = 2;i < process.argv.length; i++) {
|
|
@@ -1521,12 +2073,14 @@ function parseCli() {
|
|
|
1521
2073
|
|
|
1522
2074
|
Usage:
|
|
1523
2075
|
code-viewer [--cwd <repo>] [--port <port>] [--open] [git-diff-args...]
|
|
2076
|
+
code-viewer annotate <start|add|list|delete|clear> [options]
|
|
1524
2077
|
|
|
1525
2078
|
Examples:
|
|
1526
2079
|
code-viewer --open
|
|
1527
2080
|
code-viewer --cwd /path/to/repo --open
|
|
1528
2081
|
code-viewer HEAD~1 HEAD
|
|
1529
2082
|
code-viewer --staged
|
|
2083
|
+
code-viewer annotate --help
|
|
1530
2084
|
`);
|
|
1531
2085
|
process.exit(0);
|
|
1532
2086
|
} else if (arg === "--version" || arg === "-v") {
|
|
@@ -1539,7 +2093,7 @@ Examples:
|
|
|
1539
2093
|
process.exit(1);
|
|
1540
2094
|
}
|
|
1541
2095
|
try {
|
|
1542
|
-
cwd = repoRoot(next) ||
|
|
2096
|
+
cwd = repoRoot(next) || realpathSync2(next);
|
|
1543
2097
|
} catch {
|
|
1544
2098
|
console.error("--cwd must point to an existing directory");
|
|
1545
2099
|
process.exit(1);
|
|
@@ -1648,10 +2202,10 @@ function staticFile(pathname) {
|
|
|
1648
2202
|
const spec = map[pathname];
|
|
1649
2203
|
if (!spec)
|
|
1650
2204
|
return null;
|
|
1651
|
-
const full =
|
|
1652
|
-
if (!
|
|
2205
|
+
const full = join7(WEB_ROOT, spec[0]);
|
|
2206
|
+
if (!existsSync5(full))
|
|
1653
2207
|
return text("not found", 404);
|
|
1654
|
-
return new Response(
|
|
2208
|
+
return new Response(readFileSync5(full), {
|
|
1655
2209
|
headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
|
|
1656
2210
|
});
|
|
1657
2211
|
}
|
|
@@ -1882,21 +2436,21 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
1882
2436
|
return normalizeScopeExcludeNames(names);
|
|
1883
2437
|
}
|
|
1884
2438
|
function loadProjectConfig() {
|
|
1885
|
-
const full =
|
|
1886
|
-
if (!
|
|
2439
|
+
const full = join7(cwd, ".code-viewer.json");
|
|
2440
|
+
if (!existsSync5(full))
|
|
1887
2441
|
return null;
|
|
1888
2442
|
let realCwd;
|
|
1889
2443
|
let realConfig;
|
|
1890
2444
|
try {
|
|
1891
|
-
realCwd =
|
|
1892
|
-
realConfig =
|
|
2445
|
+
realCwd = realpathSync2(cwd);
|
|
2446
|
+
realConfig = realpathSync2(full);
|
|
1893
2447
|
} catch {
|
|
1894
2448
|
return null;
|
|
1895
2449
|
}
|
|
1896
2450
|
if (dirname2(realConfig) !== realCwd || basename2(realConfig) !== ".code-viewer.json")
|
|
1897
2451
|
return null;
|
|
1898
2452
|
try {
|
|
1899
|
-
const parsed = JSON.parse(
|
|
2453
|
+
const parsed = JSON.parse(readFileSync5(realConfig, "utf8"));
|
|
1900
2454
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
|
|
1901
2455
|
return null;
|
|
1902
2456
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
@@ -1947,14 +2501,14 @@ function safeWorktreePath(path) {
|
|
|
1947
2501
|
return null;
|
|
1948
2502
|
if (isGitInternalPath(path))
|
|
1949
2503
|
return null;
|
|
1950
|
-
const full =
|
|
1951
|
-
if (!
|
|
2504
|
+
const full = join7(cwd, path);
|
|
2505
|
+
if (!existsSync5(full))
|
|
1952
2506
|
return null;
|
|
1953
2507
|
let realCwd;
|
|
1954
2508
|
let realFull;
|
|
1955
2509
|
try {
|
|
1956
|
-
realCwd =
|
|
1957
|
-
realFull =
|
|
2510
|
+
realCwd = realpathSync2(cwd);
|
|
2511
|
+
realFull = realpathSync2(full);
|
|
1958
2512
|
} catch {
|
|
1959
2513
|
return null;
|
|
1960
2514
|
}
|
|
@@ -1966,12 +2520,12 @@ function safeWorktreePath(path) {
|
|
|
1966
2520
|
return realFull;
|
|
1967
2521
|
}
|
|
1968
2522
|
function worktreePath(path) {
|
|
1969
|
-
return
|
|
2523
|
+
return join7(cwd, path);
|
|
1970
2524
|
}
|
|
1971
2525
|
function safeOpenWorktreePath(path) {
|
|
1972
2526
|
if (path === "") {
|
|
1973
2527
|
try {
|
|
1974
|
-
const realCwd =
|
|
2528
|
+
const realCwd = realpathSync2(cwd);
|
|
1975
2529
|
if (isGitInternalPath(realCwd))
|
|
1976
2530
|
return null;
|
|
1977
2531
|
return realCwd;
|
|
@@ -2059,7 +2613,7 @@ function readReadme(target, dirPath) {
|
|
|
2059
2613
|
if (!full)
|
|
2060
2614
|
continue;
|
|
2061
2615
|
try {
|
|
2062
|
-
return { path, text:
|
|
2616
|
+
return { path, text: readFileSync5(full, "utf8") };
|
|
2063
2617
|
} catch {
|
|
2064
2618
|
continue;
|
|
2065
2619
|
}
|
|
@@ -2171,7 +2725,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
|
2171
2725
|
continue;
|
|
2172
2726
|
let data;
|
|
2173
2727
|
try {
|
|
2174
|
-
data =
|
|
2728
|
+
data = readFileSync5(full);
|
|
2175
2729
|
} catch {
|
|
2176
2730
|
continue;
|
|
2177
2731
|
}
|
|
@@ -2729,10 +3283,10 @@ async function handleUploadFiles(req) {
|
|
|
2729
3283
|
total += file.size;
|
|
2730
3284
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
2731
3285
|
return text("upload too large", 413);
|
|
2732
|
-
const target =
|
|
3286
|
+
const target = join7(realDir, safeName);
|
|
2733
3287
|
if (relative2(realDir, dirname2(target)) !== "")
|
|
2734
3288
|
return text("invalid filename", 400);
|
|
2735
|
-
if (
|
|
3289
|
+
if (existsSync5(target))
|
|
2736
3290
|
return text("file exists", 409);
|
|
2737
3291
|
uploads.push({ file, name: safeName, target });
|
|
2738
3292
|
}
|
|
@@ -2741,7 +3295,7 @@ async function handleUploadFiles(req) {
|
|
|
2741
3295
|
for (const upload of uploads) {
|
|
2742
3296
|
const fd = openSync(upload.target, uploadOpenFlags(), 420);
|
|
2743
3297
|
try {
|
|
2744
|
-
|
|
3298
|
+
writeFileSync3(fd, new Uint8Array(await upload.file.arrayBuffer()));
|
|
2745
3299
|
} finally {
|
|
2746
3300
|
closeSync(fd);
|
|
2747
3301
|
}
|
|
@@ -2750,7 +3304,7 @@ async function handleUploadFiles(req) {
|
|
|
2750
3304
|
} catch (error) {
|
|
2751
3305
|
for (const path of written) {
|
|
2752
3306
|
try {
|
|
2753
|
-
|
|
3307
|
+
unlinkSync2(path);
|
|
2754
3308
|
} catch {}
|
|
2755
3309
|
}
|
|
2756
3310
|
if (error.code === "EEXIST")
|
|
@@ -2850,12 +3404,12 @@ function triggerUpdate() {
|
|
|
2850
3404
|
sendSse("update");
|
|
2851
3405
|
}
|
|
2852
3406
|
function moveMacPathIntoTrash(path) {
|
|
2853
|
-
const trashDir =
|
|
3407
|
+
const trashDir = join7(homedir2(), ".Trash");
|
|
2854
3408
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
2855
|
-
const target =
|
|
3409
|
+
const target = join7(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2856
3410
|
try {
|
|
2857
|
-
|
|
2858
|
-
|
|
3411
|
+
mkdirSync3(trashDir, { recursive: true });
|
|
3412
|
+
renameSync2(path, target);
|
|
2859
3413
|
return { ok: true, trashPath: target };
|
|
2860
3414
|
} catch (error) {
|
|
2861
3415
|
return { ok: false, error: String(error) };
|
|
@@ -2886,20 +3440,20 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
2886
3440
|
if (!parentFullPath)
|
|
2887
3441
|
return { ok: false, error: "invalid restore target" };
|
|
2888
3442
|
const original = worktreePath(originalPath);
|
|
2889
|
-
if (
|
|
3443
|
+
if (existsSync5(original))
|
|
2890
3444
|
return { ok: false, error: "restore target exists" };
|
|
2891
3445
|
if (trashPath) {
|
|
2892
3446
|
if (process.platform !== "darwin")
|
|
2893
3447
|
return { ok: false, error: "invalid trash handle" };
|
|
2894
|
-
if (!
|
|
3448
|
+
if (!existsSync5(trashPath))
|
|
2895
3449
|
return { ok: false, error: "trash item not found" };
|
|
2896
3450
|
try {
|
|
2897
|
-
const trashRoot =
|
|
3451
|
+
const trashRoot = join7(homedir2(), ".Trash");
|
|
2898
3452
|
const trashRelative = relative2(trashRoot, trashPath);
|
|
2899
3453
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2900
3454
|
return { ok: false, error: "invalid trash handle" };
|
|
2901
|
-
|
|
2902
|
-
|
|
3455
|
+
mkdirSync3(dirname2(original), { recursive: true });
|
|
3456
|
+
renameSync2(trashPath, original);
|
|
2903
3457
|
return { ok: true };
|
|
2904
3458
|
} catch (error) {
|
|
2905
3459
|
return { ok: false, error: String(error) };
|
|
@@ -3044,11 +3598,11 @@ async function handleCreateDirectory(req) {
|
|
|
3044
3598
|
const targetPath = dir ? `${dir}/${name}` : name;
|
|
3045
3599
|
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
3046
3600
|
return text("invalid target", 400);
|
|
3047
|
-
const target =
|
|
3048
|
-
if (
|
|
3601
|
+
const target = join7(parent, name);
|
|
3602
|
+
if (existsSync5(target))
|
|
3049
3603
|
return text("already exists", 409);
|
|
3050
3604
|
try {
|
|
3051
|
-
|
|
3605
|
+
mkdirSync3(target, { recursive: false });
|
|
3052
3606
|
} catch (error) {
|
|
3053
3607
|
if (error.code === "EEXIST")
|
|
3054
3608
|
return text("already exists", 409);
|
|
@@ -3089,6 +3643,88 @@ async function handleRestoreTrash(req) {
|
|
|
3089
3643
|
triggerUpdate();
|
|
3090
3644
|
return json({ ok: true, generation });
|
|
3091
3645
|
}
|
|
3646
|
+
function annotationSse(kind, sessionId, entryId) {
|
|
3647
|
+
sendSse("annotation", JSON.stringify({ kind, session_id: sessionId, entry_id: entryId }));
|
|
3648
|
+
}
|
|
3649
|
+
async function handleAnnotations(req) {
|
|
3650
|
+
if (req.method === "GET")
|
|
3651
|
+
return json(loadAnnotationsState(cwd));
|
|
3652
|
+
if (req.method !== "POST")
|
|
3653
|
+
return text("method not allowed", 405);
|
|
3654
|
+
if (!sideEffectRequestAllowed(req))
|
|
3655
|
+
return text("forbidden", 403);
|
|
3656
|
+
const contentType = req.headers.get("content-type") || "";
|
|
3657
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
3658
|
+
return text("unsupported media type", 415);
|
|
3659
|
+
const maxBytes = ANNOTATION_BODY_MAX_BYTES + 4096;
|
|
3660
|
+
const length = Number(req.headers.get("content-length") || "0");
|
|
3661
|
+
if (length > maxBytes)
|
|
3662
|
+
return text("payload too large", 413);
|
|
3663
|
+
let body = {};
|
|
3664
|
+
try {
|
|
3665
|
+
const raw = await req.text();
|
|
3666
|
+
if (raw.length > maxBytes)
|
|
3667
|
+
return text("payload too large", 413);
|
|
3668
|
+
body = JSON.parse(raw);
|
|
3669
|
+
} catch {
|
|
3670
|
+
return text("invalid json", 400);
|
|
3671
|
+
}
|
|
3672
|
+
const action = body.action;
|
|
3673
|
+
if (action === "start") {
|
|
3674
|
+
const title = typeof body.title === "string" ? body.title : "";
|
|
3675
|
+
const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
|
|
3676
|
+
saveAnnotationsState(cwd, started.state);
|
|
3677
|
+
annotationSse("start", started.session.id);
|
|
3678
|
+
return json({ ok: true, session: started.session });
|
|
3679
|
+
}
|
|
3680
|
+
if (action === "add") {
|
|
3681
|
+
const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
|
|
3682
|
+
if (!path || !safeRepoPath(path))
|
|
3683
|
+
return text("invalid path", 400);
|
|
3684
|
+
if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
|
|
3685
|
+
return text("forbidden", 403);
|
|
3686
|
+
const result = addAnnotationEntry(loadAnnotationsState(cwd), {
|
|
3687
|
+
session_id: typeof body.session_id === "string" ? body.session_id : undefined,
|
|
3688
|
+
session_title: typeof body.session_title === "string" ? body.session_title : undefined,
|
|
3689
|
+
path,
|
|
3690
|
+
line: body.line && typeof body.line === "object" ? body.line : undefined,
|
|
3691
|
+
range: body.range && typeof body.range === "object" ? body.range : undefined,
|
|
3692
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
3693
|
+
body: typeof body.body === "string" ? body.body : ""
|
|
3694
|
+
}, new Date().toISOString());
|
|
3695
|
+
if (result.ok === false)
|
|
3696
|
+
return text(result.error, 400);
|
|
3697
|
+
saveAnnotationsState(cwd, result.state);
|
|
3698
|
+
annotationSse("add", result.session.id, result.entry.id);
|
|
3699
|
+
return json({
|
|
3700
|
+
ok: true,
|
|
3701
|
+
session_id: result.session.id,
|
|
3702
|
+
session_title: result.session.title,
|
|
3703
|
+
created_session: result.created_session,
|
|
3704
|
+
entry: result.entry
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
if (action === "delete") {
|
|
3708
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
3709
|
+
if (!id)
|
|
3710
|
+
return text("invalid id", 400);
|
|
3711
|
+
const result = deleteAnnotationById(loadAnnotationsState(cwd), id);
|
|
3712
|
+
if (result.removed) {
|
|
3713
|
+
saveAnnotationsState(cwd, result.state);
|
|
3714
|
+
annotationSse("delete");
|
|
3715
|
+
}
|
|
3716
|
+
return json({ ok: true, removed: result.removed });
|
|
3717
|
+
}
|
|
3718
|
+
if (action === "clear") {
|
|
3719
|
+
saveAnnotationsState(cwd, emptyAnnotationsState());
|
|
3720
|
+
annotationSse("clear");
|
|
3721
|
+
return json({ ok: true });
|
|
3722
|
+
}
|
|
3723
|
+
return text("invalid action", 400);
|
|
3724
|
+
}
|
|
3725
|
+
function isCodeViewerInternalPath(path) {
|
|
3726
|
+
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
|
|
3727
|
+
}
|
|
3092
3728
|
function sendSse(event, data = "tick") {
|
|
3093
3729
|
const payload = enc.encode(`event: ${event}
|
|
3094
3730
|
data: ${data}
|
|
@@ -3106,112 +3742,205 @@ function openBrowser(url) {
|
|
|
3106
3742
|
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd.exe", "/c", "start", "", url] : ["xdg-open", url];
|
|
3107
3743
|
spawnDetached(cmd);
|
|
3108
3744
|
}
|
|
3109
|
-
|
|
3110
|
-
var
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3745
|
+
var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, uploadDisabledByConfig = false, rgAvailableCache = null, enc, sseClients, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, server;
|
|
3746
|
+
var init_preview = __esm(async () => {
|
|
3747
|
+
init_routes();
|
|
3748
|
+
init_annotations();
|
|
3749
|
+
init_cache();
|
|
3750
|
+
init_dev_assets();
|
|
3751
|
+
init_git();
|
|
3752
|
+
init_root();
|
|
3753
|
+
init_runtime();
|
|
3754
|
+
init_search();
|
|
3755
|
+
init_server_registry();
|
|
3756
|
+
init_worktree_watcher();
|
|
3757
|
+
WEB_ROOT = join7(ROOT, "web");
|
|
3758
|
+
VERSION = JSON.parse(readFileSync5(join7(ROOT, "package.json"), "utf8")).version;
|
|
3759
|
+
DEFAULT_ARGS = ["HEAD"];
|
|
3760
|
+
WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
|
|
3761
|
+
LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
3762
|
+
BLOB_LINE_CACHE_MAX_BYTES = 128 * 1024 * 1024;
|
|
3763
|
+
MAX_UPLOAD_FILE_BYTES = 512 * 1024 * 1024;
|
|
3764
|
+
MAX_UPLOAD_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
3765
|
+
MAX_UPLOAD_BODY_BYTES = MAX_UPLOAD_TOTAL_BYTES + 1024 * 1024;
|
|
3766
|
+
SAFE_UPLOAD_EXTENSIONS = new Set([
|
|
3767
|
+
".txt",
|
|
3768
|
+
".md",
|
|
3769
|
+
".markdown",
|
|
3770
|
+
".json",
|
|
3771
|
+
".csv",
|
|
3772
|
+
".tsv",
|
|
3773
|
+
".yaml",
|
|
3774
|
+
".yml",
|
|
3775
|
+
".toml",
|
|
3776
|
+
".png",
|
|
3777
|
+
".jpg",
|
|
3778
|
+
".jpeg",
|
|
3779
|
+
".gif",
|
|
3780
|
+
".webp",
|
|
3781
|
+
".svg",
|
|
3782
|
+
".pdf",
|
|
3783
|
+
".mp4",
|
|
3784
|
+
".mov",
|
|
3785
|
+
".m4v",
|
|
3786
|
+
".webm",
|
|
3787
|
+
".mp3",
|
|
3788
|
+
".wav",
|
|
3789
|
+
".m4a",
|
|
3790
|
+
".aac",
|
|
3791
|
+
".flac",
|
|
3792
|
+
".ogg",
|
|
3793
|
+
".zip",
|
|
3794
|
+
".ts",
|
|
3795
|
+
".tsx",
|
|
3796
|
+
".js",
|
|
3797
|
+
".jsx",
|
|
3798
|
+
".css",
|
|
3799
|
+
".scss",
|
|
3800
|
+
".html"
|
|
3801
|
+
]);
|
|
3802
|
+
cwd = repoRoot(process.cwd()) || process.cwd();
|
|
3803
|
+
cliArgs = DEFAULT_ARGS;
|
|
3804
|
+
scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
3805
|
+
scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
3806
|
+
enc = new TextEncoder;
|
|
3807
|
+
sseClients = new Set;
|
|
3808
|
+
fileCache = new Map;
|
|
3809
|
+
metaCache = new Map;
|
|
3810
|
+
fileListCache = new Map;
|
|
3811
|
+
lineIndexCache = new Map;
|
|
3812
|
+
blobLineIndexCache = new Map;
|
|
3813
|
+
blobBytesCache = new Map;
|
|
3814
|
+
parseCli();
|
|
3815
|
+
server = await startServer({
|
|
3816
|
+
hostname: "127.0.0.1",
|
|
3817
|
+
port: listenPort,
|
|
3818
|
+
async fetch(req) {
|
|
3819
|
+
if (!requestAllowed(req))
|
|
3152
3820
|
return text("forbidden", 403);
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3821
|
+
const url = new URL(req.url);
|
|
3822
|
+
const staticResponse = staticFile(url.pathname);
|
|
3823
|
+
if (staticResponse)
|
|
3824
|
+
return staticResponse;
|
|
3825
|
+
if (url.pathname === "/diff.json")
|
|
3826
|
+
return handleDiffJson(url);
|
|
3827
|
+
if (url.pathname === "/_settings")
|
|
3828
|
+
return handleSettings();
|
|
3829
|
+
if (url.pathname === "/_tree")
|
|
3830
|
+
return handleTree(url);
|
|
3831
|
+
if (url.pathname === "/_files")
|
|
3832
|
+
return handleFiles(url);
|
|
3833
|
+
if (url.pathname === "/_grep")
|
|
3834
|
+
return handleGrep(url);
|
|
3835
|
+
if (url.pathname === "/_commits")
|
|
3836
|
+
return handleRefCommits(url);
|
|
3837
|
+
if (url.pathname === "/file_diff")
|
|
3838
|
+
return handleFileDiff(url);
|
|
3839
|
+
if (url.pathname === "/file_range")
|
|
3840
|
+
return handleFileRange(url);
|
|
3841
|
+
if (url.pathname === "/_file")
|
|
3842
|
+
return handleRawFile(req, url);
|
|
3843
|
+
if (url.pathname === "/_open_path")
|
|
3844
|
+
return handleOpenPath(req);
|
|
3845
|
+
if (url.pathname === "/_trash_path")
|
|
3846
|
+
return handleTrashPath(req);
|
|
3847
|
+
if (url.pathname === "/_restore_trash")
|
|
3848
|
+
return handleRestoreTrash(req);
|
|
3849
|
+
if (url.pathname === "/_create_directory")
|
|
3850
|
+
return handleCreateDirectory(req);
|
|
3851
|
+
if (url.pathname === "/_upload_files")
|
|
3852
|
+
return handleUploadFiles(req);
|
|
3853
|
+
if (url.pathname === "/_annotations")
|
|
3854
|
+
return handleAnnotations(req);
|
|
3855
|
+
if (url.pathname === "/_refs")
|
|
3856
|
+
return json(refs(cwd));
|
|
3857
|
+
if (url.pathname === "/refresh" && req.method === "POST") {
|
|
3858
|
+
if (!sideEffectRequestAllowed(req))
|
|
3859
|
+
return text("forbidden", 403);
|
|
3860
|
+
triggerUpdate();
|
|
3861
|
+
return json({ ok: true, generation });
|
|
3862
|
+
}
|
|
3863
|
+
if (url.pathname === "/events") {
|
|
3864
|
+
let ctrl;
|
|
3865
|
+
let keepalive;
|
|
3866
|
+
return new Response(new ReadableStream({
|
|
3867
|
+
start(controller) {
|
|
3868
|
+
ctrl = controller;
|
|
3869
|
+
sseClients.add(controller);
|
|
3870
|
+
controller.enqueue(enc.encode(`event: open
|
|
3164
3871
|
data: ok
|
|
3165
3872
|
|
|
3166
3873
|
`));
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3874
|
+
keepalive = setInterval(() => {
|
|
3875
|
+
try {
|
|
3876
|
+
controller.enqueue(enc.encode(`: ping
|
|
3170
3877
|
|
|
3171
3878
|
`));
|
|
3172
|
-
|
|
3173
|
-
|
|
3879
|
+
} catch {
|
|
3880
|
+
sseClients.delete(controller);
|
|
3881
|
+
clearInterval(keepalive);
|
|
3882
|
+
}
|
|
3883
|
+
}, 15000);
|
|
3884
|
+
},
|
|
3885
|
+
cancel() {
|
|
3886
|
+
if (ctrl)
|
|
3887
|
+
sseClients.delete(ctrl);
|
|
3888
|
+
if (keepalive)
|
|
3174
3889
|
clearInterval(keepalive);
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
}), {
|
|
3185
|
-
headers: {
|
|
3186
|
-
"Content-Type": "text/event-stream",
|
|
3187
|
-
"Cache-Control": "no-cache"
|
|
3188
|
-
}
|
|
3189
|
-
});
|
|
3890
|
+
}
|
|
3891
|
+
}), {
|
|
3892
|
+
headers: {
|
|
3893
|
+
"Content-Type": "text/event-stream",
|
|
3894
|
+
"Cache-Control": "no-cache"
|
|
3895
|
+
}
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
return text("not found", 404);
|
|
3190
3899
|
}
|
|
3191
|
-
|
|
3900
|
+
});
|
|
3901
|
+
if (openAfterStart) {
|
|
3902
|
+
openBrowser(`http://127.0.0.1:${server.port}/`);
|
|
3903
|
+
}
|
|
3904
|
+
writeServerRegistry({
|
|
3905
|
+
url: `http://127.0.0.1:${server.port}/`,
|
|
3906
|
+
pid: process.pid,
|
|
3907
|
+
root: cwd,
|
|
3908
|
+
started_at: new Date().toISOString()
|
|
3909
|
+
});
|
|
3910
|
+
process.on("exit", () => removeServerRegistry(cwd, process.pid));
|
|
3911
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
3912
|
+
process.on(signal, () => {
|
|
3913
|
+
removeServerRegistry(cwd, process.pid);
|
|
3914
|
+
process.exit(0);
|
|
3915
|
+
});
|
|
3192
3916
|
}
|
|
3917
|
+
startDevAssetReload({
|
|
3918
|
+
enabled: process.env.CODE_VIEWER_DEV === "1",
|
|
3919
|
+
webRoot: WEB_ROOT,
|
|
3920
|
+
watchedFiles: WATCHED_ASSET_FILES,
|
|
3921
|
+
watch,
|
|
3922
|
+
sendReload: () => sendSse("reload")
|
|
3923
|
+
});
|
|
3924
|
+
startWorktreeUpdateWatch({
|
|
3925
|
+
root: cwd,
|
|
3926
|
+
omitDirNames: scopeOmitDirNames,
|
|
3927
|
+
excludeNames: scopeExcludeNames,
|
|
3928
|
+
watch,
|
|
3929
|
+
initialScanMode: "async",
|
|
3930
|
+
onUpdate: triggerUpdate,
|
|
3931
|
+
onError: (error) => {
|
|
3932
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3933
|
+
console.warn(`code-viewer worktree watch skipped: ${message}`);
|
|
3934
|
+
}
|
|
3935
|
+
});
|
|
3936
|
+
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
3937
|
+
console.log(`git-diff-preview serving ${cwd}`);
|
|
3193
3938
|
});
|
|
3194
|
-
|
|
3195
|
-
|
|
3939
|
+
|
|
3940
|
+
// web-src/server/cli.ts
|
|
3941
|
+
if (process.argv[2] === "annotate") {
|
|
3942
|
+
const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
|
|
3943
|
+
await runAnnotateCli2(process.argv.slice(3));
|
|
3944
|
+
} else {
|
|
3945
|
+
await init_preview().then(() => exports_preview);
|
|
3196
3946
|
}
|
|
3197
|
-
startDevAssetReload({
|
|
3198
|
-
enabled: process.env.CODE_VIEWER_DEV === "1",
|
|
3199
|
-
webRoot: WEB_ROOT,
|
|
3200
|
-
watchedFiles: WATCHED_ASSET_FILES,
|
|
3201
|
-
watch,
|
|
3202
|
-
sendReload: () => sendSse("reload")
|
|
3203
|
-
});
|
|
3204
|
-
startWorktreeUpdateWatch({
|
|
3205
|
-
root: cwd,
|
|
3206
|
-
omitDirNames: scopeOmitDirNames,
|
|
3207
|
-
excludeNames: scopeExcludeNames,
|
|
3208
|
-
watch,
|
|
3209
|
-
initialScanMode: "async",
|
|
3210
|
-
onUpdate: triggerUpdate,
|
|
3211
|
-
onError: (error) => {
|
|
3212
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3213
|
-
console.warn(`code-viewer worktree watch skipped: ${message}`);
|
|
3214
|
-
}
|
|
3215
|
-
});
|
|
3216
|
-
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
3217
|
-
console.log(`git-diff-preview serving ${cwd}`);
|