@youtyan/code-viewer 0.1.33 → 0.1.35
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 +46 -0
- package/dist/code-viewer.js +1149 -351
- package/package.json +1 -1
- package/web/app.js +4374 -3910
- package/web/index.html +28 -0
- package/web/style.css +301 -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,581 @@ 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
|
+
ANNOTATE_AGENT_HELP: () => ANNOTATE_AGENT_HELP
|
|
1111
|
+
});
|
|
1112
|
+
import { readFileSync as readFileSync4, realpathSync } from "node:fs";
|
|
1113
|
+
function takeValue(argv, index, flag) {
|
|
1114
|
+
const value = argv[index + 1];
|
|
1115
|
+
if (value === undefined)
|
|
1116
|
+
return { error: `${flag} requires a value` };
|
|
1117
|
+
return { value, next: index + 1 };
|
|
1118
|
+
}
|
|
1119
|
+
function parseAnnotateArgs(argv) {
|
|
1120
|
+
const rest = [];
|
|
1121
|
+
let cwd;
|
|
1122
|
+
let server;
|
|
1123
|
+
const options = new Map;
|
|
1124
|
+
const flags = new Set;
|
|
1125
|
+
const valueFlags = new Set([
|
|
1126
|
+
"--title",
|
|
1127
|
+
"--file",
|
|
1128
|
+
"--line",
|
|
1129
|
+
"--from",
|
|
1130
|
+
"--to",
|
|
1131
|
+
"--session",
|
|
1132
|
+
"--session-title",
|
|
1133
|
+
"--body",
|
|
1134
|
+
"--body-file"
|
|
1135
|
+
]);
|
|
1136
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1137
|
+
const arg = argv[i];
|
|
1138
|
+
if (arg === "--help" || arg === "-h")
|
|
1139
|
+
return { ok: true, args: { command: { kind: "help" } } };
|
|
1140
|
+
if (arg === "--cwd" || arg === "--server") {
|
|
1141
|
+
const taken = takeValue(argv, i, arg);
|
|
1142
|
+
if ("error" in taken)
|
|
1143
|
+
return { ok: false, error: taken.error };
|
|
1144
|
+
if (arg === "--cwd")
|
|
1145
|
+
cwd = taken.value;
|
|
1146
|
+
else
|
|
1147
|
+
server = taken.value;
|
|
1148
|
+
i = taken.next;
|
|
1149
|
+
} else if (valueFlags.has(arg)) {
|
|
1150
|
+
const taken = takeValue(argv, i, arg);
|
|
1151
|
+
if ("error" in taken)
|
|
1152
|
+
return { ok: false, error: taken.error };
|
|
1153
|
+
options.set(arg, taken.value);
|
|
1154
|
+
i = taken.next;
|
|
1155
|
+
} else if (arg === "--json") {
|
|
1156
|
+
flags.add(arg);
|
|
1157
|
+
} else if (arg.startsWith("-")) {
|
|
1158
|
+
return { ok: false, error: `unknown option: ${arg}` };
|
|
1159
|
+
} else {
|
|
1160
|
+
rest.push(arg);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const subcommand = rest[0];
|
|
1164
|
+
if (!subcommand)
|
|
1165
|
+
return { ok: true, args: { command: { kind: "help" } } };
|
|
1166
|
+
if (subcommand === "agent-help") {
|
|
1167
|
+
return { ok: true, args: { command: { kind: "agent-help" } } };
|
|
1168
|
+
}
|
|
1169
|
+
if (subcommand === "start") {
|
|
1170
|
+
return {
|
|
1171
|
+
ok: true,
|
|
1172
|
+
args: {
|
|
1173
|
+
command: { kind: "start", title: options.get("--title") || "" },
|
|
1174
|
+
cwd,
|
|
1175
|
+
server
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
if (subcommand === "add") {
|
|
1180
|
+
const file = options.get("--file");
|
|
1181
|
+
if (!file)
|
|
1182
|
+
return { ok: false, error: "add requires --file <path>" };
|
|
1183
|
+
let line;
|
|
1184
|
+
const rawLine = options.get("--line");
|
|
1185
|
+
if (rawLine !== undefined) {
|
|
1186
|
+
line = parseAnnotationLine(rawLine);
|
|
1187
|
+
if (!line)
|
|
1188
|
+
return { ok: false, error: "--line must be <n> or <n>-<m>" };
|
|
1189
|
+
}
|
|
1190
|
+
const body = options.get("--body");
|
|
1191
|
+
const bodyFile = options.get("--body-file");
|
|
1192
|
+
if (body !== undefined && bodyFile !== undefined)
|
|
1193
|
+
return { ok: false, error: "use either --body or --body-file" };
|
|
1194
|
+
return {
|
|
1195
|
+
ok: true,
|
|
1196
|
+
args: {
|
|
1197
|
+
command: {
|
|
1198
|
+
kind: "add",
|
|
1199
|
+
file,
|
|
1200
|
+
line,
|
|
1201
|
+
from: options.get("--from"),
|
|
1202
|
+
to: options.get("--to"),
|
|
1203
|
+
title: options.get("--title"),
|
|
1204
|
+
session: options.get("--session"),
|
|
1205
|
+
sessionTitle: options.get("--session-title"),
|
|
1206
|
+
body,
|
|
1207
|
+
bodyFile
|
|
1208
|
+
},
|
|
1209
|
+
cwd,
|
|
1210
|
+
server
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
if (subcommand === "list") {
|
|
1215
|
+
return {
|
|
1216
|
+
ok: true,
|
|
1217
|
+
args: {
|
|
1218
|
+
command: { kind: "list", json: flags.has("--json") },
|
|
1219
|
+
cwd,
|
|
1220
|
+
server
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
if (subcommand === "delete") {
|
|
1225
|
+
const id = rest[1];
|
|
1226
|
+
if (!id)
|
|
1227
|
+
return { ok: false, error: "delete requires an id" };
|
|
1228
|
+
return { ok: true, args: { command: { kind: "delete", id }, cwd, server } };
|
|
1229
|
+
}
|
|
1230
|
+
if (subcommand === "clear") {
|
|
1231
|
+
return { ok: true, args: { command: { kind: "clear" }, cwd, server } };
|
|
1232
|
+
}
|
|
1233
|
+
return { ok: false, error: `unknown annotate command: ${subcommand}` };
|
|
1234
|
+
}
|
|
1235
|
+
async function readStdin() {
|
|
1236
|
+
if (process.stdin.isTTY)
|
|
1237
|
+
return "";
|
|
1238
|
+
const chunks = [];
|
|
1239
|
+
for await (const chunk of process.stdin) {
|
|
1240
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1241
|
+
}
|
|
1242
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1243
|
+
}
|
|
1244
|
+
function resolveRepoRoot(cwdOption) {
|
|
1245
|
+
const base = cwdOption || process.cwd();
|
|
1246
|
+
try {
|
|
1247
|
+
return repoRoot(base) || realpathSync(base);
|
|
1248
|
+
} catch {
|
|
1249
|
+
console.error(`--cwd must point to an existing directory: ${base}`);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async function serverReachable(serverUrl) {
|
|
1254
|
+
try {
|
|
1255
|
+
const res = await fetch(`${serverUrl}/_annotations`, {
|
|
1256
|
+
signal: AbortSignal.timeout(1500)
|
|
1257
|
+
});
|
|
1258
|
+
return res.ok;
|
|
1259
|
+
} catch {
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async function ensureServerUrl(root, override) {
|
|
1264
|
+
if (override) {
|
|
1265
|
+
const url = override.replace(/\/+$/, "");
|
|
1266
|
+
if (await serverReachable(url))
|
|
1267
|
+
return url;
|
|
1268
|
+
console.error(`could not reach the code-viewer server at ${url}.`);
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
const registered = readServerRegistry(root);
|
|
1272
|
+
if (registered) {
|
|
1273
|
+
const url = registered.url.replace(/\/+$/, "");
|
|
1274
|
+
if (await serverReachable(url))
|
|
1275
|
+
return url;
|
|
1276
|
+
}
|
|
1277
|
+
console.error(`no running code-viewer server for this repository.
|
|
1278
|
+
` + `Start one manually (from ${root}):
|
|
1279
|
+
` + " code-viewer");
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
async function request(serverUrl, method, body) {
|
|
1283
|
+
const url = `${serverUrl}/_annotations`;
|
|
1284
|
+
const origin = new URL(serverUrl).origin;
|
|
1285
|
+
let res;
|
|
1286
|
+
try {
|
|
1287
|
+
res = await fetch(url, {
|
|
1288
|
+
method,
|
|
1289
|
+
headers: method === "POST" ? {
|
|
1290
|
+
"Content-Type": "application/json",
|
|
1291
|
+
Origin: origin,
|
|
1292
|
+
"X-Code-Viewer-Action": "1"
|
|
1293
|
+
} : {},
|
|
1294
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1295
|
+
});
|
|
1296
|
+
} catch {
|
|
1297
|
+
console.error(`could not reach the code-viewer server at ${serverUrl}.`);
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
if (!res.ok) {
|
|
1301
|
+
console.error(`server rejected the request: ${await res.text()}`);
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
1304
|
+
return res.json();
|
|
1305
|
+
}
|
|
1306
|
+
function formatLine(line) {
|
|
1307
|
+
if (!line)
|
|
1308
|
+
return "";
|
|
1309
|
+
return line.start === line.end ? `:${line.start}` : `:${line.start}-${line.end}`;
|
|
1310
|
+
}
|
|
1311
|
+
function printList(state) {
|
|
1312
|
+
if (!state.sessions.length) {
|
|
1313
|
+
console.log("no annotations");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
for (const session of state.sessions) {
|
|
1317
|
+
console.log(`session ${session.id} ${session.title}`);
|
|
1318
|
+
session.entries.forEach((entry, index) => {
|
|
1319
|
+
const location = `${entry.path}${formatLine(entry.line)}`;
|
|
1320
|
+
const summary = (entry.title || entry.body).split(`
|
|
1321
|
+
`)[0].slice(0, 80);
|
|
1322
|
+
console.log(` ${index + 1}. [${entry.id}] ${location} ${summary}`);
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function runAnnotateCli(argv) {
|
|
1327
|
+
const parsed = parseAnnotateArgs(argv);
|
|
1328
|
+
if (parsed.ok === false) {
|
|
1329
|
+
console.error(parsed.error);
|
|
1330
|
+
console.error('Run "code-viewer annotate --help" for usage.');
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
const { command, cwd, server } = parsed.args;
|
|
1334
|
+
if (command.kind === "help") {
|
|
1335
|
+
console.log(ANNOTATE_HELP);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (command.kind === "agent-help") {
|
|
1339
|
+
console.log(ANNOTATE_AGENT_HELP);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const root = resolveRepoRoot(cwd);
|
|
1343
|
+
const serverUrl = await ensureServerUrl(root, server);
|
|
1344
|
+
if (command.kind === "start") {
|
|
1345
|
+
const result = await request(serverUrl, "POST", {
|
|
1346
|
+
action: "start",
|
|
1347
|
+
title: command.title
|
|
1348
|
+
});
|
|
1349
|
+
console.log(`session ${result.session.id} ${result.session.title}`);
|
|
1350
|
+
console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (command.kind === "add") {
|
|
1354
|
+
let body = command.body;
|
|
1355
|
+
if (body === undefined && command.bodyFile !== undefined) {
|
|
1356
|
+
try {
|
|
1357
|
+
body = readFileSync4(command.bodyFile, "utf8");
|
|
1358
|
+
} catch {
|
|
1359
|
+
console.error(`could not read --body-file: ${command.bodyFile}`);
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (body === undefined)
|
|
1364
|
+
body = await readStdin();
|
|
1365
|
+
if (!body.trim()) {
|
|
1366
|
+
console.error("annotation body is empty. Pass --body, --body-file, or pipe stdin.");
|
|
1367
|
+
process.exit(1);
|
|
1368
|
+
}
|
|
1369
|
+
const result = await request(serverUrl, "POST", {
|
|
1370
|
+
action: "add",
|
|
1371
|
+
session_id: command.session,
|
|
1372
|
+
session_title: command.sessionTitle,
|
|
1373
|
+
path: command.file,
|
|
1374
|
+
line: command.line,
|
|
1375
|
+
range: { from: command.from, to: command.to },
|
|
1376
|
+
title: command.title,
|
|
1377
|
+
body
|
|
1378
|
+
});
|
|
1379
|
+
if (result.created_session) {
|
|
1380
|
+
console.error(`created new annotation session ${result.session_id} (${result.session_title || "Untitled session"})`);
|
|
1381
|
+
}
|
|
1382
|
+
console.log(`annotated ${result.entry.path}${formatLine(result.entry.line)} ` + `[${result.entry.id}] in session ${result.session_id} (${result.session_title || "Untitled session"})`);
|
|
1383
|
+
console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
if (command.kind === "list") {
|
|
1387
|
+
const state = await request(serverUrl, "GET");
|
|
1388
|
+
if (command.json)
|
|
1389
|
+
console.log(JSON.stringify(state, null, 2));
|
|
1390
|
+
else
|
|
1391
|
+
printList(state);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (command.kind === "delete") {
|
|
1395
|
+
const result = await request(serverUrl, "POST", {
|
|
1396
|
+
action: "delete",
|
|
1397
|
+
id: command.id
|
|
1398
|
+
});
|
|
1399
|
+
if (!result.removed) {
|
|
1400
|
+
console.error(`no annotation or session with id ${command.id}`);
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
console.log(`deleted ${result.removed} ${command.id}`);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
await request(serverUrl, "POST", { action: "clear" });
|
|
1407
|
+
console.log("cleared all annotations");
|
|
1408
|
+
}
|
|
1409
|
+
var ANNOTATE_HELP = `code-viewer annotate — attach explanations to code locations
|
|
1410
|
+
|
|
1411
|
+
The annotations show up live in the code-viewer browser UI and are stored
|
|
1412
|
+
in <repo>/.code-viewer/annotations.json. A running code-viewer server for
|
|
1413
|
+
the repository is required: start one with "code-viewer" before using
|
|
1414
|
+
annotate (or point at one explicitly with --server).
|
|
1415
|
+
|
|
1416
|
+
Run "code-viewer annotate agent-help" for an AI-agent oriented guide
|
|
1417
|
+
(workflow, conventions, and pitfalls for writing good walkthroughs).
|
|
1418
|
+
|
|
1419
|
+
Usage:
|
|
1420
|
+
code-viewer annotate start [--title <text>]
|
|
1421
|
+
code-viewer annotate add --file <path> [--line <n>|<n>-<m>]
|
|
1422
|
+
[--from <ref>] [--to <ref>] [--title <text>] [--session <id>]
|
|
1423
|
+
[--body <markdown> | --body-file <path>] (or pipe body via stdin)
|
|
1424
|
+
code-viewer annotate list [--json]
|
|
1425
|
+
code-viewer annotate delete <id>
|
|
1426
|
+
code-viewer annotate clear
|
|
1427
|
+
|
|
1428
|
+
Global options:
|
|
1429
|
+
--cwd <dir> repository to annotate (default: current directory)
|
|
1430
|
+
--server <url> code-viewer server URL (default: auto-discovered)
|
|
1431
|
+
|
|
1432
|
+
Examples:
|
|
1433
|
+
code-viewer annotate start --title "How SSE updates work"
|
|
1434
|
+
code-viewer annotate add --file web-src/server/preview.ts --line 2220-2250 \\
|
|
1435
|
+
--body "This endpoint keeps one SSE stream per browser tab."
|
|
1436
|
+
git diff HEAD~1 | code-viewer annotate add --file src/app.ts --line 10 \\
|
|
1437
|
+
--from HEAD~1 --to worktree --body "The fix moves the guard up here."
|
|
1438
|
+
`, ANNOTATE_AGENT_HELP = `code-viewer annotate — agent guide
|
|
1439
|
+
|
|
1440
|
+
You are an AI coding agent. Use this tool to walk a human through code in
|
|
1441
|
+
their browser: each annotation jumps every open code-viewer tab to a file
|
|
1442
|
+
location and renders your explanation directly under the annotated lines.
|
|
1443
|
+
|
|
1444
|
+
## When to use
|
|
1445
|
+
|
|
1446
|
+
- Explaining a change you just made (per-file, per-hunk commentary)
|
|
1447
|
+
- Guiding a code review: point at the risky lines, in reading order
|
|
1448
|
+
- Onboarding walkthroughs: "how does feature X flow through the code"
|
|
1449
|
+
|
|
1450
|
+
## Requirements
|
|
1451
|
+
|
|
1452
|
+
- A code-viewer server must already be running for the repository
|
|
1453
|
+
(the human starts it with: code-viewer). This command never starts one.
|
|
1454
|
+
- Run from inside the repository, or pass --cwd <repo>.
|
|
1455
|
+
- If "code-viewer" is not on PATH (e.g. the human runs it via npx), invoke
|
|
1456
|
+
every command below as: npx -y @youtyan/code-viewer annotate ...
|
|
1457
|
+
|
|
1458
|
+
## Workflow
|
|
1459
|
+
|
|
1460
|
+
1. Start a session per walkthrough topic. The title is shown to the human:
|
|
1461
|
+
code-viewer annotate start --title "How the cache invalidation works"
|
|
1462
|
+
2. Add annotations in READING ORDER (the order you want the human to
|
|
1463
|
+
follow). Each add without --session appends to the most recent session:
|
|
1464
|
+
code-viewer annotate add --file src/cache.ts --line 120-145 \\
|
|
1465
|
+
--title "Entry point" --body "Writes land here first. ..."
|
|
1466
|
+
3. Verify what you posted:
|
|
1467
|
+
code-viewer annotate list
|
|
1468
|
+
|
|
1469
|
+
## Conventions for good walkthroughs
|
|
1470
|
+
|
|
1471
|
+
- One idea per annotation. Prefer 5-10 focused annotations over 2 huge ones.
|
|
1472
|
+
- Always pass --line. Use the smallest range that covers the idea; the
|
|
1473
|
+
body is rendered inline directly under the LAST line of the range.
|
|
1474
|
+
- Line numbers must match the "to" side of the range (default: the current
|
|
1475
|
+
worktree state of the file). When annotating a diff against another ref,
|
|
1476
|
+
pass --from/--to and use NEW-side line numbers.
|
|
1477
|
+
- The body is Markdown. Code spans, fenced blocks, and links work. Long
|
|
1478
|
+
bodies: use --body-file <path> or pipe via stdin instead of --body.
|
|
1479
|
+
- Give every annotation a short --title; it becomes the inline heading.
|
|
1480
|
+
- Annotating unchanged code is fine: the viewer auto-expands diff context
|
|
1481
|
+
or falls back to the full source view.
|
|
1482
|
+
|
|
1483
|
+
## Sessions
|
|
1484
|
+
|
|
1485
|
+
- add (no --session) → appends to the most recent session.
|
|
1486
|
+
- annotate start → begins a NEW session; later adds go there.
|
|
1487
|
+
- add --session <id> → targets a specific session (ids: annotate list).
|
|
1488
|
+
- The human can share a walkthrough as a URL; one session = one shareable
|
|
1489
|
+
walkthrough. Do not mix unrelated topics in one session.
|
|
1490
|
+
|
|
1491
|
+
## Cleanup
|
|
1492
|
+
|
|
1493
|
+
- delete <id> removes one annotation or a whole session by its id.
|
|
1494
|
+
- clear removes everything. Ask the human before clearing state you did
|
|
1495
|
+
not create.
|
|
1496
|
+
`;
|
|
1497
|
+
var init_annotate_cli = __esm(() => {
|
|
1498
|
+
init_annotations();
|
|
1499
|
+
init_git();
|
|
1500
|
+
init_server_registry();
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
// web-src/directory-name.ts
|
|
1504
|
+
function normalizeNewDirectoryName(name) {
|
|
1505
|
+
if (typeof name !== "string")
|
|
1506
|
+
return null;
|
|
1507
|
+
const trimmed = name.trim();
|
|
1508
|
+
if (!trimmed || trimmed.length > 180)
|
|
1509
|
+
return null;
|
|
1510
|
+
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\x00") || Array.from(trimmed).some((char) => {
|
|
1511
|
+
const code = char.charCodeAt(0);
|
|
1512
|
+
return code < 32 || code === 127;
|
|
1513
|
+
}))
|
|
1514
|
+
return null;
|
|
1515
|
+
if (trimmed === "." || trimmed === ".." || trimmed.toLowerCase() === ".git")
|
|
1516
|
+
return null;
|
|
1517
|
+
return trimmed;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// web-src/routes.ts
|
|
1521
|
+
var SPA_PATHS, APP_ENTRY_PATHS;
|
|
1522
|
+
var init_routes = __esm(() => {
|
|
1523
|
+
SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
|
|
1524
|
+
APP_ENTRY_PATHS = ["/", "/index.html"];
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
// web-src/server/cache.ts
|
|
1528
|
+
import { lstatSync as lstatSync2 } from "node:fs";
|
|
1529
|
+
import { join as join4 } from "node:path";
|
|
1530
|
+
function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
|
|
1531
|
+
return !!cached && now - cached.storedAt <= ttlMs;
|
|
1532
|
+
}
|
|
1533
|
+
function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
|
|
1534
|
+
cache.set(key, { ...value, storedAt: now });
|
|
1535
|
+
while (cache.size > maxEntries) {
|
|
1536
|
+
const oldest = cache.keys().next().value;
|
|
1537
|
+
if (oldest === undefined)
|
|
1538
|
+
break;
|
|
1539
|
+
cache.delete(oldest);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function worktreeFileSignature(path, cwd) {
|
|
1543
|
+
try {
|
|
1544
|
+
const stats = lstatSync2(join4(cwd, path));
|
|
1545
|
+
const inode = "ino" in stats ? stats.ino : 0;
|
|
1546
|
+
return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
|
|
1547
|
+
} catch {
|
|
1548
|
+
return "state:missing";
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
function fileDiffCacheKey(options) {
|
|
1552
|
+
const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
|
|
1553
|
+
if (options.isUntracked && !worktreeTarget) {
|
|
1554
|
+
throw new Error("untracked file diffs require a worktree range");
|
|
1555
|
+
}
|
|
1556
|
+
const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
|
|
1557
|
+
if (options.isUntracked) {
|
|
1558
|
+
return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
|
|
1559
|
+
}
|
|
1560
|
+
return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
|
|
1561
|
+
}
|
|
1562
|
+
var CACHE_TTL_MS = 1500, MAX_TIMED_CACHE_ENTRIES = 200;
|
|
1563
|
+
var init_cache = () => {};
|
|
1564
|
+
|
|
1565
|
+
// web-src/server/dev-assets.ts
|
|
1566
|
+
import { basename } from "node:path";
|
|
1567
|
+
function startDevAssetReload(options) {
|
|
1568
|
+
if (!options.enabled)
|
|
1569
|
+
return false;
|
|
1570
|
+
const watched = new Set(options.watchedFiles);
|
|
1571
|
+
const setTimer = options.setTimeoutFn || setTimeout;
|
|
1572
|
+
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
1573
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
1574
|
+
let timer = null;
|
|
1575
|
+
options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
|
|
1576
|
+
if (!filename || !watched.has(basename(filename.toString())))
|
|
1577
|
+
return;
|
|
1578
|
+
if (timer)
|
|
1579
|
+
clearTimer(timer);
|
|
1580
|
+
timer = setTimer(() => {
|
|
1581
|
+
timer = null;
|
|
1582
|
+
options.sendReload();
|
|
1583
|
+
}, debounceMs);
|
|
1584
|
+
});
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
var init_dev_assets = () => {};
|
|
920
1588
|
|
|
921
1589
|
// web-src/server/range.ts
|
|
922
1590
|
function isSameWorktreeRange(range) {
|
|
@@ -1130,13 +1798,13 @@ function collectLineRangeFromIndexedText(text, index, start, end) {
|
|
|
1130
1798
|
}
|
|
1131
1799
|
|
|
1132
1800
|
// web-src/server/root.ts
|
|
1133
|
-
import { existsSync as
|
|
1134
|
-
import { dirname, join as
|
|
1801
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1802
|
+
import { dirname, join as join5, normalize } from "node:path";
|
|
1135
1803
|
import { fileURLToPath } from "node:url";
|
|
1136
1804
|
function findRoot(start) {
|
|
1137
1805
|
let current = start;
|
|
1138
1806
|
for (let i = 0;i < 5; i++) {
|
|
1139
|
-
if (
|
|
1807
|
+
if (existsSync4(join5(current, "package.json")) && existsSync4(join5(current, "web"))) {
|
|
1140
1808
|
return normalize(current);
|
|
1141
1809
|
}
|
|
1142
1810
|
const parent = dirname(current);
|
|
@@ -1144,16 +1812,14 @@ function findRoot(start) {
|
|
|
1144
1812
|
break;
|
|
1145
1813
|
current = parent;
|
|
1146
1814
|
}
|
|
1147
|
-
return normalize(
|
|
1815
|
+
return normalize(join5(start, "..", ".."));
|
|
1148
1816
|
}
|
|
1149
|
-
var ROOT
|
|
1817
|
+
var ROOT;
|
|
1818
|
+
var init_root = __esm(() => {
|
|
1819
|
+
ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
|
|
1820
|
+
});
|
|
1150
1821
|
|
|
1151
1822
|
// 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
1823
|
function normalizeGrepMax(value) {
|
|
1158
1824
|
const parsed = Number(value || "");
|
|
1159
1825
|
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
@@ -1165,7 +1831,7 @@ function isSkippableSearchPath(path, omitDirNames = [], excludeNames = []) {
|
|
|
1165
1831
|
const excluded = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
1166
1832
|
return path.split(/[\\/]+/).some((part) => {
|
|
1167
1833
|
const lower = part.toLowerCase();
|
|
1168
|
-
return lower === ".git" || omitDirs.has(lower) || excluded.has(lower);
|
|
1834
|
+
return lower === ".git" || lower === ".code-viewer" || omitDirs.has(lower) || excluded.has(lower);
|
|
1169
1835
|
});
|
|
1170
1836
|
}
|
|
1171
1837
|
function fixedStringLineMatches(path, text, query, max) {
|
|
@@ -1267,6 +1933,11 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
|
|
|
1267
1933
|
`);
|
|
1268
1934
|
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1269
1935
|
}
|
|
1936
|
+
var GREP_DEFAULT_MAX = 200, GREP_ABSOLUTE_MAX = 500, GREP_MAX_FILE_BYTES, FILE_SEARCH_ABSOLUTE_MAX = 50000, DEFAULT_EXCLUDE_NAMES;
|
|
1937
|
+
var init_search = __esm(() => {
|
|
1938
|
+
GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1939
|
+
DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
|
|
1940
|
+
});
|
|
1270
1941
|
|
|
1271
1942
|
// web-src/server/worktree-watcher.ts
|
|
1272
1943
|
import {
|
|
@@ -1274,7 +1945,7 @@ import {
|
|
|
1274
1945
|
readdirSync as nodeReaddirSync,
|
|
1275
1946
|
watch as nodeWatch
|
|
1276
1947
|
} from "node:fs";
|
|
1277
|
-
import { join as
|
|
1948
|
+
import { join as join6, relative } from "node:path";
|
|
1278
1949
|
function normalizeRelativePath(path) {
|
|
1279
1950
|
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1280
1951
|
}
|
|
@@ -1357,7 +2028,7 @@ function startWorktreeUpdateWatch(options) {
|
|
|
1357
2028
|
for (const entry of entries) {
|
|
1358
2029
|
if (!entry.isDirectory())
|
|
1359
2030
|
continue;
|
|
1360
|
-
children.push(
|
|
2031
|
+
children.push(join6(dir, entry.name));
|
|
1361
2032
|
}
|
|
1362
2033
|
return children;
|
|
1363
2034
|
};
|
|
@@ -1386,10 +2057,10 @@ function startWorktreeUpdateWatch(options) {
|
|
|
1386
2057
|
scheduleUpdate();
|
|
1387
2058
|
return;
|
|
1388
2059
|
}
|
|
1389
|
-
const changed = normalizeRelativePath(
|
|
2060
|
+
const changed = normalizeRelativePath(join6(rel, filename.toString()));
|
|
1390
2061
|
if (ignored(changed))
|
|
1391
2062
|
return;
|
|
1392
|
-
const fullChangedPath =
|
|
2063
|
+
const fullChangedPath = join6(options.root, changed);
|
|
1393
2064
|
if (!isInsideRoot(options.root, fullChangedPath))
|
|
1394
2065
|
return;
|
|
1395
2066
|
const known = watchers.has(fullChangedPath);
|
|
@@ -1439,79 +2110,29 @@ function startWorktreeUpdateWatch(options) {
|
|
|
1439
2110
|
watchDirectory(options.root, true);
|
|
1440
2111
|
return { started: watchers.size > 0, close: closeAll };
|
|
1441
2112
|
}
|
|
2113
|
+
var init_worktree_watcher = __esm(() => {
|
|
2114
|
+
init_search();
|
|
2115
|
+
});
|
|
1442
2116
|
|
|
1443
2117
|
// 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;
|
|
2118
|
+
var exports_preview = {};
|
|
2119
|
+
import {
|
|
2120
|
+
closeSync,
|
|
2121
|
+
constants,
|
|
2122
|
+
existsSync as existsSync5,
|
|
2123
|
+
lstatSync as lstatSync4,
|
|
2124
|
+
mkdirSync as mkdirSync3,
|
|
2125
|
+
openSync,
|
|
2126
|
+
readFileSync as readFileSync5,
|
|
2127
|
+
realpathSync as realpathSync2,
|
|
2128
|
+
renameSync as renameSync2,
|
|
2129
|
+
statSync as statSync2,
|
|
2130
|
+
unlinkSync as unlinkSync2,
|
|
2131
|
+
watch,
|
|
2132
|
+
writeFileSync as writeFileSync3
|
|
2133
|
+
} from "node:fs";
|
|
2134
|
+
import { homedir as homedir2 } from "node:os";
|
|
2135
|
+
import { basename as basename2, dirname as dirname2, extname, join as join7, relative as relative2 } from "node:path";
|
|
1515
2136
|
function parseCli() {
|
|
1516
2137
|
const rest = [];
|
|
1517
2138
|
for (let i = 2;i < process.argv.length; i++) {
|
|
@@ -1521,12 +2142,14 @@ function parseCli() {
|
|
|
1521
2142
|
|
|
1522
2143
|
Usage:
|
|
1523
2144
|
code-viewer [--cwd <repo>] [--port <port>] [--open] [git-diff-args...]
|
|
2145
|
+
code-viewer annotate <start|add|list|delete|clear> [options]
|
|
1524
2146
|
|
|
1525
2147
|
Examples:
|
|
1526
2148
|
code-viewer --open
|
|
1527
2149
|
code-viewer --cwd /path/to/repo --open
|
|
1528
2150
|
code-viewer HEAD~1 HEAD
|
|
1529
2151
|
code-viewer --staged
|
|
2152
|
+
code-viewer annotate --help
|
|
1530
2153
|
`);
|
|
1531
2154
|
process.exit(0);
|
|
1532
2155
|
} else if (arg === "--version" || arg === "-v") {
|
|
@@ -1539,7 +2162,7 @@ Examples:
|
|
|
1539
2162
|
process.exit(1);
|
|
1540
2163
|
}
|
|
1541
2164
|
try {
|
|
1542
|
-
cwd = repoRoot(next) ||
|
|
2165
|
+
cwd = repoRoot(next) || realpathSync2(next);
|
|
1543
2166
|
} catch {
|
|
1544
2167
|
console.error("--cwd must point to an existing directory");
|
|
1545
2168
|
process.exit(1);
|
|
@@ -1648,10 +2271,10 @@ function staticFile(pathname) {
|
|
|
1648
2271
|
const spec = map[pathname];
|
|
1649
2272
|
if (!spec)
|
|
1650
2273
|
return null;
|
|
1651
|
-
const full =
|
|
1652
|
-
if (!
|
|
2274
|
+
const full = join7(WEB_ROOT, spec[0]);
|
|
2275
|
+
if (!existsSync5(full))
|
|
1653
2276
|
return text("not found", 404);
|
|
1654
|
-
return new Response(
|
|
2277
|
+
return new Response(readFileSync5(full), {
|
|
1655
2278
|
headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
|
|
1656
2279
|
});
|
|
1657
2280
|
}
|
|
@@ -1882,21 +2505,21 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
1882
2505
|
return normalizeScopeExcludeNames(names);
|
|
1883
2506
|
}
|
|
1884
2507
|
function loadProjectConfig() {
|
|
1885
|
-
const full =
|
|
1886
|
-
if (!
|
|
2508
|
+
const full = join7(cwd, ".code-viewer.json");
|
|
2509
|
+
if (!existsSync5(full))
|
|
1887
2510
|
return null;
|
|
1888
2511
|
let realCwd;
|
|
1889
2512
|
let realConfig;
|
|
1890
2513
|
try {
|
|
1891
|
-
realCwd =
|
|
1892
|
-
realConfig =
|
|
2514
|
+
realCwd = realpathSync2(cwd);
|
|
2515
|
+
realConfig = realpathSync2(full);
|
|
1893
2516
|
} catch {
|
|
1894
2517
|
return null;
|
|
1895
2518
|
}
|
|
1896
2519
|
if (dirname2(realConfig) !== realCwd || basename2(realConfig) !== ".code-viewer.json")
|
|
1897
2520
|
return null;
|
|
1898
2521
|
try {
|
|
1899
|
-
const parsed = JSON.parse(
|
|
2522
|
+
const parsed = JSON.parse(readFileSync5(realConfig, "utf8"));
|
|
1900
2523
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "version" in parsed && parsed.version !== 1)
|
|
1901
2524
|
return null;
|
|
1902
2525
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
@@ -1947,14 +2570,14 @@ function safeWorktreePath(path) {
|
|
|
1947
2570
|
return null;
|
|
1948
2571
|
if (isGitInternalPath(path))
|
|
1949
2572
|
return null;
|
|
1950
|
-
const full =
|
|
1951
|
-
if (!
|
|
2573
|
+
const full = join7(cwd, path);
|
|
2574
|
+
if (!existsSync5(full))
|
|
1952
2575
|
return null;
|
|
1953
2576
|
let realCwd;
|
|
1954
2577
|
let realFull;
|
|
1955
2578
|
try {
|
|
1956
|
-
realCwd =
|
|
1957
|
-
realFull =
|
|
2579
|
+
realCwd = realpathSync2(cwd);
|
|
2580
|
+
realFull = realpathSync2(full);
|
|
1958
2581
|
} catch {
|
|
1959
2582
|
return null;
|
|
1960
2583
|
}
|
|
@@ -1966,12 +2589,12 @@ function safeWorktreePath(path) {
|
|
|
1966
2589
|
return realFull;
|
|
1967
2590
|
}
|
|
1968
2591
|
function worktreePath(path) {
|
|
1969
|
-
return
|
|
2592
|
+
return join7(cwd, path);
|
|
1970
2593
|
}
|
|
1971
2594
|
function safeOpenWorktreePath(path) {
|
|
1972
2595
|
if (path === "") {
|
|
1973
2596
|
try {
|
|
1974
|
-
const realCwd =
|
|
2597
|
+
const realCwd = realpathSync2(cwd);
|
|
1975
2598
|
if (isGitInternalPath(realCwd))
|
|
1976
2599
|
return null;
|
|
1977
2600
|
return realCwd;
|
|
@@ -2059,7 +2682,7 @@ function readReadme(target, dirPath) {
|
|
|
2059
2682
|
if (!full)
|
|
2060
2683
|
continue;
|
|
2061
2684
|
try {
|
|
2062
|
-
return { path, text:
|
|
2685
|
+
return { path, text: readFileSync5(full, "utf8") };
|
|
2063
2686
|
} catch {
|
|
2064
2687
|
continue;
|
|
2065
2688
|
}
|
|
@@ -2171,7 +2794,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
|
2171
2794
|
continue;
|
|
2172
2795
|
let data;
|
|
2173
2796
|
try {
|
|
2174
|
-
data =
|
|
2797
|
+
data = readFileSync5(full);
|
|
2175
2798
|
} catch {
|
|
2176
2799
|
continue;
|
|
2177
2800
|
}
|
|
@@ -2729,10 +3352,10 @@ async function handleUploadFiles(req) {
|
|
|
2729
3352
|
total += file.size;
|
|
2730
3353
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
2731
3354
|
return text("upload too large", 413);
|
|
2732
|
-
const target =
|
|
3355
|
+
const target = join7(realDir, safeName);
|
|
2733
3356
|
if (relative2(realDir, dirname2(target)) !== "")
|
|
2734
3357
|
return text("invalid filename", 400);
|
|
2735
|
-
if (
|
|
3358
|
+
if (existsSync5(target))
|
|
2736
3359
|
return text("file exists", 409);
|
|
2737
3360
|
uploads.push({ file, name: safeName, target });
|
|
2738
3361
|
}
|
|
@@ -2741,7 +3364,7 @@ async function handleUploadFiles(req) {
|
|
|
2741
3364
|
for (const upload of uploads) {
|
|
2742
3365
|
const fd = openSync(upload.target, uploadOpenFlags(), 420);
|
|
2743
3366
|
try {
|
|
2744
|
-
|
|
3367
|
+
writeFileSync3(fd, new Uint8Array(await upload.file.arrayBuffer()));
|
|
2745
3368
|
} finally {
|
|
2746
3369
|
closeSync(fd);
|
|
2747
3370
|
}
|
|
@@ -2750,7 +3373,7 @@ async function handleUploadFiles(req) {
|
|
|
2750
3373
|
} catch (error) {
|
|
2751
3374
|
for (const path of written) {
|
|
2752
3375
|
try {
|
|
2753
|
-
|
|
3376
|
+
unlinkSync2(path);
|
|
2754
3377
|
} catch {}
|
|
2755
3378
|
}
|
|
2756
3379
|
if (error.code === "EEXIST")
|
|
@@ -2850,12 +3473,12 @@ function triggerUpdate() {
|
|
|
2850
3473
|
sendSse("update");
|
|
2851
3474
|
}
|
|
2852
3475
|
function moveMacPathIntoTrash(path) {
|
|
2853
|
-
const trashDir =
|
|
3476
|
+
const trashDir = join7(homedir2(), ".Trash");
|
|
2854
3477
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
2855
|
-
const target =
|
|
3478
|
+
const target = join7(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2856
3479
|
try {
|
|
2857
|
-
|
|
2858
|
-
|
|
3480
|
+
mkdirSync3(trashDir, { recursive: true });
|
|
3481
|
+
renameSync2(path, target);
|
|
2859
3482
|
return { ok: true, trashPath: target };
|
|
2860
3483
|
} catch (error) {
|
|
2861
3484
|
return { ok: false, error: String(error) };
|
|
@@ -2886,20 +3509,20 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
2886
3509
|
if (!parentFullPath)
|
|
2887
3510
|
return { ok: false, error: "invalid restore target" };
|
|
2888
3511
|
const original = worktreePath(originalPath);
|
|
2889
|
-
if (
|
|
3512
|
+
if (existsSync5(original))
|
|
2890
3513
|
return { ok: false, error: "restore target exists" };
|
|
2891
3514
|
if (trashPath) {
|
|
2892
3515
|
if (process.platform !== "darwin")
|
|
2893
3516
|
return { ok: false, error: "invalid trash handle" };
|
|
2894
|
-
if (!
|
|
3517
|
+
if (!existsSync5(trashPath))
|
|
2895
3518
|
return { ok: false, error: "trash item not found" };
|
|
2896
3519
|
try {
|
|
2897
|
-
const trashRoot =
|
|
3520
|
+
const trashRoot = join7(homedir2(), ".Trash");
|
|
2898
3521
|
const trashRelative = relative2(trashRoot, trashPath);
|
|
2899
3522
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2900
3523
|
return { ok: false, error: "invalid trash handle" };
|
|
2901
|
-
|
|
2902
|
-
|
|
3524
|
+
mkdirSync3(dirname2(original), { recursive: true });
|
|
3525
|
+
renameSync2(trashPath, original);
|
|
2903
3526
|
return { ok: true };
|
|
2904
3527
|
} catch (error) {
|
|
2905
3528
|
return { ok: false, error: String(error) };
|
|
@@ -3044,11 +3667,11 @@ async function handleCreateDirectory(req) {
|
|
|
3044
3667
|
const targetPath = dir ? `${dir}/${name}` : name;
|
|
3045
3668
|
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
3046
3669
|
return text("invalid target", 400);
|
|
3047
|
-
const target =
|
|
3048
|
-
if (
|
|
3670
|
+
const target = join7(parent, name);
|
|
3671
|
+
if (existsSync5(target))
|
|
3049
3672
|
return text("already exists", 409);
|
|
3050
3673
|
try {
|
|
3051
|
-
|
|
3674
|
+
mkdirSync3(target, { recursive: false });
|
|
3052
3675
|
} catch (error) {
|
|
3053
3676
|
if (error.code === "EEXIST")
|
|
3054
3677
|
return text("already exists", 409);
|
|
@@ -3089,6 +3712,88 @@ async function handleRestoreTrash(req) {
|
|
|
3089
3712
|
triggerUpdate();
|
|
3090
3713
|
return json({ ok: true, generation });
|
|
3091
3714
|
}
|
|
3715
|
+
function annotationSse(kind, sessionId, entryId) {
|
|
3716
|
+
sendSse("annotation", JSON.stringify({ kind, session_id: sessionId, entry_id: entryId }));
|
|
3717
|
+
}
|
|
3718
|
+
async function handleAnnotations(req) {
|
|
3719
|
+
if (req.method === "GET")
|
|
3720
|
+
return json(loadAnnotationsState(cwd));
|
|
3721
|
+
if (req.method !== "POST")
|
|
3722
|
+
return text("method not allowed", 405);
|
|
3723
|
+
if (!sideEffectRequestAllowed(req))
|
|
3724
|
+
return text("forbidden", 403);
|
|
3725
|
+
const contentType = req.headers.get("content-type") || "";
|
|
3726
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
3727
|
+
return text("unsupported media type", 415);
|
|
3728
|
+
const maxBytes = ANNOTATION_BODY_MAX_BYTES + 4096;
|
|
3729
|
+
const length = Number(req.headers.get("content-length") || "0");
|
|
3730
|
+
if (length > maxBytes)
|
|
3731
|
+
return text("payload too large", 413);
|
|
3732
|
+
let body = {};
|
|
3733
|
+
try {
|
|
3734
|
+
const raw = await req.text();
|
|
3735
|
+
if (raw.length > maxBytes)
|
|
3736
|
+
return text("payload too large", 413);
|
|
3737
|
+
body = JSON.parse(raw);
|
|
3738
|
+
} catch {
|
|
3739
|
+
return text("invalid json", 400);
|
|
3740
|
+
}
|
|
3741
|
+
const action = body.action;
|
|
3742
|
+
if (action === "start") {
|
|
3743
|
+
const title = typeof body.title === "string" ? body.title : "";
|
|
3744
|
+
const started = startAnnotationSession(loadAnnotationsState(cwd), title, new Date().toISOString());
|
|
3745
|
+
saveAnnotationsState(cwd, started.state);
|
|
3746
|
+
annotationSse("start", started.session.id);
|
|
3747
|
+
return json({ ok: true, session: started.session });
|
|
3748
|
+
}
|
|
3749
|
+
if (action === "add") {
|
|
3750
|
+
const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
|
|
3751
|
+
if (!path || !safeRepoPath(path))
|
|
3752
|
+
return text("invalid path", 400);
|
|
3753
|
+
if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
|
|
3754
|
+
return text("forbidden", 403);
|
|
3755
|
+
const result = addAnnotationEntry(loadAnnotationsState(cwd), {
|
|
3756
|
+
session_id: typeof body.session_id === "string" ? body.session_id : undefined,
|
|
3757
|
+
session_title: typeof body.session_title === "string" ? body.session_title : undefined,
|
|
3758
|
+
path,
|
|
3759
|
+
line: body.line && typeof body.line === "object" ? body.line : undefined,
|
|
3760
|
+
range: body.range && typeof body.range === "object" ? body.range : undefined,
|
|
3761
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
3762
|
+
body: typeof body.body === "string" ? body.body : ""
|
|
3763
|
+
}, new Date().toISOString());
|
|
3764
|
+
if (result.ok === false)
|
|
3765
|
+
return text(result.error, 400);
|
|
3766
|
+
saveAnnotationsState(cwd, result.state);
|
|
3767
|
+
annotationSse("add", result.session.id, result.entry.id);
|
|
3768
|
+
return json({
|
|
3769
|
+
ok: true,
|
|
3770
|
+
session_id: result.session.id,
|
|
3771
|
+
session_title: result.session.title,
|
|
3772
|
+
created_session: result.created_session,
|
|
3773
|
+
entry: result.entry
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
if (action === "delete") {
|
|
3777
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
3778
|
+
if (!id)
|
|
3779
|
+
return text("invalid id", 400);
|
|
3780
|
+
const result = deleteAnnotationById(loadAnnotationsState(cwd), id);
|
|
3781
|
+
if (result.removed) {
|
|
3782
|
+
saveAnnotationsState(cwd, result.state);
|
|
3783
|
+
annotationSse("delete");
|
|
3784
|
+
}
|
|
3785
|
+
return json({ ok: true, removed: result.removed });
|
|
3786
|
+
}
|
|
3787
|
+
if (action === "clear") {
|
|
3788
|
+
saveAnnotationsState(cwd, emptyAnnotationsState());
|
|
3789
|
+
annotationSse("clear");
|
|
3790
|
+
return json({ ok: true });
|
|
3791
|
+
}
|
|
3792
|
+
return text("invalid action", 400);
|
|
3793
|
+
}
|
|
3794
|
+
function isCodeViewerInternalPath(path) {
|
|
3795
|
+
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
|
|
3796
|
+
}
|
|
3092
3797
|
function sendSse(event, data = "tick") {
|
|
3093
3798
|
const payload = enc.encode(`event: ${event}
|
|
3094
3799
|
data: ${data}
|
|
@@ -3106,112 +3811,205 @@ function openBrowser(url) {
|
|
|
3106
3811
|
const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd.exe", "/c", "start", "", url] : ["xdg-open", url];
|
|
3107
3812
|
spawnDetached(cmd);
|
|
3108
3813
|
}
|
|
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
|
-
|
|
3814
|
+
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;
|
|
3815
|
+
var init_preview = __esm(async () => {
|
|
3816
|
+
init_routes();
|
|
3817
|
+
init_annotations();
|
|
3818
|
+
init_cache();
|
|
3819
|
+
init_dev_assets();
|
|
3820
|
+
init_git();
|
|
3821
|
+
init_root();
|
|
3822
|
+
init_runtime();
|
|
3823
|
+
init_search();
|
|
3824
|
+
init_server_registry();
|
|
3825
|
+
init_worktree_watcher();
|
|
3826
|
+
WEB_ROOT = join7(ROOT, "web");
|
|
3827
|
+
VERSION = JSON.parse(readFileSync5(join7(ROOT, "package.json"), "utf8")).version;
|
|
3828
|
+
DEFAULT_ARGS = ["HEAD"];
|
|
3829
|
+
WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
|
|
3830
|
+
LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
3831
|
+
BLOB_LINE_CACHE_MAX_BYTES = 128 * 1024 * 1024;
|
|
3832
|
+
MAX_UPLOAD_FILE_BYTES = 512 * 1024 * 1024;
|
|
3833
|
+
MAX_UPLOAD_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
3834
|
+
MAX_UPLOAD_BODY_BYTES = MAX_UPLOAD_TOTAL_BYTES + 1024 * 1024;
|
|
3835
|
+
SAFE_UPLOAD_EXTENSIONS = new Set([
|
|
3836
|
+
".txt",
|
|
3837
|
+
".md",
|
|
3838
|
+
".markdown",
|
|
3839
|
+
".json",
|
|
3840
|
+
".csv",
|
|
3841
|
+
".tsv",
|
|
3842
|
+
".yaml",
|
|
3843
|
+
".yml",
|
|
3844
|
+
".toml",
|
|
3845
|
+
".png",
|
|
3846
|
+
".jpg",
|
|
3847
|
+
".jpeg",
|
|
3848
|
+
".gif",
|
|
3849
|
+
".webp",
|
|
3850
|
+
".svg",
|
|
3851
|
+
".pdf",
|
|
3852
|
+
".mp4",
|
|
3853
|
+
".mov",
|
|
3854
|
+
".m4v",
|
|
3855
|
+
".webm",
|
|
3856
|
+
".mp3",
|
|
3857
|
+
".wav",
|
|
3858
|
+
".m4a",
|
|
3859
|
+
".aac",
|
|
3860
|
+
".flac",
|
|
3861
|
+
".ogg",
|
|
3862
|
+
".zip",
|
|
3863
|
+
".ts",
|
|
3864
|
+
".tsx",
|
|
3865
|
+
".js",
|
|
3866
|
+
".jsx",
|
|
3867
|
+
".css",
|
|
3868
|
+
".scss",
|
|
3869
|
+
".html"
|
|
3870
|
+
]);
|
|
3871
|
+
cwd = repoRoot(process.cwd()) || process.cwd();
|
|
3872
|
+
cliArgs = DEFAULT_ARGS;
|
|
3873
|
+
scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
3874
|
+
scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
3875
|
+
enc = new TextEncoder;
|
|
3876
|
+
sseClients = new Set;
|
|
3877
|
+
fileCache = new Map;
|
|
3878
|
+
metaCache = new Map;
|
|
3879
|
+
fileListCache = new Map;
|
|
3880
|
+
lineIndexCache = new Map;
|
|
3881
|
+
blobLineIndexCache = new Map;
|
|
3882
|
+
blobBytesCache = new Map;
|
|
3883
|
+
parseCli();
|
|
3884
|
+
server = await startServer({
|
|
3885
|
+
hostname: "127.0.0.1",
|
|
3886
|
+
port: listenPort,
|
|
3887
|
+
async fetch(req) {
|
|
3888
|
+
if (!requestAllowed(req))
|
|
3152
3889
|
return text("forbidden", 403);
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3890
|
+
const url = new URL(req.url);
|
|
3891
|
+
const staticResponse = staticFile(url.pathname);
|
|
3892
|
+
if (staticResponse)
|
|
3893
|
+
return staticResponse;
|
|
3894
|
+
if (url.pathname === "/diff.json")
|
|
3895
|
+
return handleDiffJson(url);
|
|
3896
|
+
if (url.pathname === "/_settings")
|
|
3897
|
+
return handleSettings();
|
|
3898
|
+
if (url.pathname === "/_tree")
|
|
3899
|
+
return handleTree(url);
|
|
3900
|
+
if (url.pathname === "/_files")
|
|
3901
|
+
return handleFiles(url);
|
|
3902
|
+
if (url.pathname === "/_grep")
|
|
3903
|
+
return handleGrep(url);
|
|
3904
|
+
if (url.pathname === "/_commits")
|
|
3905
|
+
return handleRefCommits(url);
|
|
3906
|
+
if (url.pathname === "/file_diff")
|
|
3907
|
+
return handleFileDiff(url);
|
|
3908
|
+
if (url.pathname === "/file_range")
|
|
3909
|
+
return handleFileRange(url);
|
|
3910
|
+
if (url.pathname === "/_file")
|
|
3911
|
+
return handleRawFile(req, url);
|
|
3912
|
+
if (url.pathname === "/_open_path")
|
|
3913
|
+
return handleOpenPath(req);
|
|
3914
|
+
if (url.pathname === "/_trash_path")
|
|
3915
|
+
return handleTrashPath(req);
|
|
3916
|
+
if (url.pathname === "/_restore_trash")
|
|
3917
|
+
return handleRestoreTrash(req);
|
|
3918
|
+
if (url.pathname === "/_create_directory")
|
|
3919
|
+
return handleCreateDirectory(req);
|
|
3920
|
+
if (url.pathname === "/_upload_files")
|
|
3921
|
+
return handleUploadFiles(req);
|
|
3922
|
+
if (url.pathname === "/_annotations")
|
|
3923
|
+
return handleAnnotations(req);
|
|
3924
|
+
if (url.pathname === "/_refs")
|
|
3925
|
+
return json(refs(cwd));
|
|
3926
|
+
if (url.pathname === "/refresh" && req.method === "POST") {
|
|
3927
|
+
if (!sideEffectRequestAllowed(req))
|
|
3928
|
+
return text("forbidden", 403);
|
|
3929
|
+
triggerUpdate();
|
|
3930
|
+
return json({ ok: true, generation });
|
|
3931
|
+
}
|
|
3932
|
+
if (url.pathname === "/events") {
|
|
3933
|
+
let ctrl;
|
|
3934
|
+
let keepalive;
|
|
3935
|
+
return new Response(new ReadableStream({
|
|
3936
|
+
start(controller) {
|
|
3937
|
+
ctrl = controller;
|
|
3938
|
+
sseClients.add(controller);
|
|
3939
|
+
controller.enqueue(enc.encode(`event: open
|
|
3164
3940
|
data: ok
|
|
3165
3941
|
|
|
3166
3942
|
`));
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3943
|
+
keepalive = setInterval(() => {
|
|
3944
|
+
try {
|
|
3945
|
+
controller.enqueue(enc.encode(`: ping
|
|
3170
3946
|
|
|
3171
3947
|
`));
|
|
3172
|
-
|
|
3173
|
-
|
|
3948
|
+
} catch {
|
|
3949
|
+
sseClients.delete(controller);
|
|
3950
|
+
clearInterval(keepalive);
|
|
3951
|
+
}
|
|
3952
|
+
}, 15000);
|
|
3953
|
+
},
|
|
3954
|
+
cancel() {
|
|
3955
|
+
if (ctrl)
|
|
3956
|
+
sseClients.delete(ctrl);
|
|
3957
|
+
if (keepalive)
|
|
3174
3958
|
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
|
-
});
|
|
3959
|
+
}
|
|
3960
|
+
}), {
|
|
3961
|
+
headers: {
|
|
3962
|
+
"Content-Type": "text/event-stream",
|
|
3963
|
+
"Cache-Control": "no-cache"
|
|
3964
|
+
}
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
return text("not found", 404);
|
|
3190
3968
|
}
|
|
3191
|
-
|
|
3969
|
+
});
|
|
3970
|
+
if (openAfterStart) {
|
|
3971
|
+
openBrowser(`http://127.0.0.1:${server.port}/`);
|
|
3972
|
+
}
|
|
3973
|
+
writeServerRegistry({
|
|
3974
|
+
url: `http://127.0.0.1:${server.port}/`,
|
|
3975
|
+
pid: process.pid,
|
|
3976
|
+
root: cwd,
|
|
3977
|
+
started_at: new Date().toISOString()
|
|
3978
|
+
});
|
|
3979
|
+
process.on("exit", () => removeServerRegistry(cwd, process.pid));
|
|
3980
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
3981
|
+
process.on(signal, () => {
|
|
3982
|
+
removeServerRegistry(cwd, process.pid);
|
|
3983
|
+
process.exit(0);
|
|
3984
|
+
});
|
|
3192
3985
|
}
|
|
3986
|
+
startDevAssetReload({
|
|
3987
|
+
enabled: process.env.CODE_VIEWER_DEV === "1",
|
|
3988
|
+
webRoot: WEB_ROOT,
|
|
3989
|
+
watchedFiles: WATCHED_ASSET_FILES,
|
|
3990
|
+
watch,
|
|
3991
|
+
sendReload: () => sendSse("reload")
|
|
3992
|
+
});
|
|
3993
|
+
startWorktreeUpdateWatch({
|
|
3994
|
+
root: cwd,
|
|
3995
|
+
omitDirNames: scopeOmitDirNames,
|
|
3996
|
+
excludeNames: scopeExcludeNames,
|
|
3997
|
+
watch,
|
|
3998
|
+
initialScanMode: "async",
|
|
3999
|
+
onUpdate: triggerUpdate,
|
|
4000
|
+
onError: (error) => {
|
|
4001
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4002
|
+
console.warn(`code-viewer worktree watch skipped: ${message}`);
|
|
4003
|
+
}
|
|
4004
|
+
});
|
|
4005
|
+
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
4006
|
+
console.log(`git-diff-preview serving ${cwd}`);
|
|
3193
4007
|
});
|
|
3194
|
-
|
|
3195
|
-
|
|
4008
|
+
|
|
4009
|
+
// web-src/server/cli.ts
|
|
4010
|
+
if (process.argv[2] === "annotate") {
|
|
4011
|
+
const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
|
|
4012
|
+
await runAnnotateCli2(process.argv.slice(3));
|
|
4013
|
+
} else {
|
|
4014
|
+
await init_preview().then(() => exports_preview);
|
|
3196
4015
|
}
|
|
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}`);
|