@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.
@@ -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/preview.ts
18
+ // web-src/server/annotations.ts
4
19
  import {
5
- closeSync,
6
- constants,
7
- existsSync as existsSync3,
8
- lstatSync as lstatSync4,
20
+ existsSync,
9
21
  mkdirSync,
10
- openSync,
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 { homedir } from "node:os";
20
- import { basename as basename2, dirname as dirname2, extname, join as join5, relative as relative2 } from "node:path";
21
-
22
- // web-src/directory-name.ts
23
- function normalizeNewDirectoryName(name) {
24
- if (typeof name !== "string")
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 trimmed = name.trim();
27
- if (!trimmed || trimmed.length > 180)
68
+ const entry = raw;
69
+ if (typeof entry.id !== "string" || !entry.id)
28
70
  return null;
29
- if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\x00") || Array.from(trimmed).some((char) => {
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 (trimmed === "." || trimmed === ".." || trimmed.toLowerCase() === ".git")
73
+ if (typeof entry.body !== "string" || !entry.body)
35
74
  return null;
36
- return trimmed;
37
- }
38
-
39
- // web-src/routes.ts
40
- var SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
41
- var APP_ENTRY_PATHS = ["/", "/index.html"];
42
-
43
- // web-src/server/cache.ts
44
- import { lstatSync } from "node:fs";
45
- import { join } from "node:path";
46
- var CACHE_TTL_MS = 1500;
47
- var MAX_TIMED_CACHE_ENTRIES = 200;
48
- function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
49
- return !!cached && now - cached.storedAt <= ttlMs;
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 setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
52
- cache.set(key, { ...value, storedAt: now });
53
- while (cache.size > maxEntries) {
54
- const oldest = cache.keys().next().value;
55
- if (oldest === undefined)
56
- break;
57
- cache.delete(oldest);
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 worktreeFileSignature(path, cwd) {
114
+ function loadAnnotationsState(root) {
115
+ const file = annotationsFilePath(root);
116
+ if (!existsSync(file))
117
+ return emptyAnnotationsState();
61
118
  try {
62
- const stats = lstatSync(join(cwd, path));
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 "state:missing";
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 fileDiffCacheKey(options) {
70
- const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
71
- if (options.isUntracked && !worktreeTarget) {
72
- throw new Error("untracked file diffs require a worktree range");
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
- const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
75
- if (options.isUntracked) {
76
- return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
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
- return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
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
- // web-src/server/dev-assets.ts
82
- import { basename } from "node:path";
83
- function startDevAssetReload(options) {
84
- if (!options.enabled)
85
- return false;
86
- const watched = new Set(options.watchedFiles);
87
- const setTimer = options.setTimeoutFn || setTimeout;
88
- const clearTimer = options.clearTimeoutFn || clearTimeout;
89
- const debounceMs = options.debounceMs ?? 150;
90
- let timer = null;
91
- options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
92
- if (!filename || !watched.has(basename(filename.toString())))
93
- return;
94
- if (timer)
95
- clearTimer(timer);
96
- timer = setTimer(() => {
97
- timer = null;
98
- options.sendReload();
99
- }, debounceMs);
100
- });
101
- return true;
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
- // web-src/server/git.ts
105
- import {
106
- existsSync,
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
- var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
281
- var WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000;
282
- var DEFAULT_REF_COMMIT_LIMIT = 100;
283
- var MAX_REF_COMMIT_LIMIT = 500;
284
- var COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI";
285
- var DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
286
- "node_modules",
287
- ".venv",
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
- return res.code === 0 ? res.stdout.split(`
592
- `).filter(Boolean) : [];
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
- lstatSync2(join2(dir, ".git"));
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 = existsSync(full) && statSync(full).isFile();
870
+ fileExists = existsSync2(full) && statSync(full).isFile();
778
871
  } catch {
779
872
  fileExists = false;
780
873
  }
781
874
  if (fileExists) {
782
- const data = readFileSync(full);
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 existsSync2 } from "node:fs";
1134
- import { dirname, join as join3, normalize } from "node:path";
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 (existsSync2(join3(current, "package.json")) && existsSync2(join3(current, "web"))) {
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(join3(start, "..", ".."));
1815
+ return normalize(join5(start, "..", ".."));
1148
1816
  }
1149
- var ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
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 join4, relative } from "node:path";
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(join4(dir, entry.name));
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(join4(rel, filename.toString()));
2060
+ const changed = normalizeRelativePath(join6(rel, filename.toString()));
1390
2061
  if (ignored(changed))
1391
2062
  return;
1392
- const fullChangedPath = join4(options.root, changed);
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 WEB_ROOT = join5(ROOT, "web");
1445
- var VERSION = JSON.parse(readFileSync2(join5(ROOT, "package.json"), "utf8")).version;
1446
- var DEFAULT_ARGS = ["HEAD"];
1447
- var PREVIEW_HUNKS_DEFAULT = 3;
1448
- var PREVIEW_LINES_DEFAULT = 1200;
1449
- var WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
1450
- var SIZE_SMALL = 2000;
1451
- var SIZE_MEDIUM = 8000;
1452
- var SIZE_LARGE = 20000;
1453
- var LINE_INDEX_MIN_START = 1e4;
1454
- var LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
1455
- var BLOB_LINE_CACHE_MAX_BYTES = 128 * 1024 * 1024;
1456
- var MAX_UPLOAD_FILE_BYTES = 512 * 1024 * 1024;
1457
- var MAX_UPLOAD_TOTAL_BYTES = 1024 * 1024 * 1024;
1458
- var MAX_UPLOAD_BODY_BYTES = MAX_UPLOAD_TOTAL_BYTES + 1024 * 1024;
1459
- var MAX_UPLOAD_FILES = 50;
1460
- var SAFE_UPLOAD_EXTENSIONS = new Set([
1461
- ".txt",
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) || realpathSync(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 = join5(WEB_ROOT, spec[0]);
1652
- if (!existsSync3(full))
2274
+ const full = join7(WEB_ROOT, spec[0]);
2275
+ if (!existsSync5(full))
1653
2276
  return text("not found", 404);
1654
- return new Response(readFileSync2(full), {
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 = join5(cwd, ".code-viewer.json");
1886
- if (!existsSync3(full))
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 = realpathSync(cwd);
1892
- realConfig = realpathSync(full);
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(readFileSync2(realConfig, "utf8"));
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 = join5(cwd, path);
1951
- if (!existsSync3(full))
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 = realpathSync(cwd);
1957
- realFull = realpathSync(full);
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 join5(cwd, path);
2592
+ return join7(cwd, path);
1970
2593
  }
1971
2594
  function safeOpenWorktreePath(path) {
1972
2595
  if (path === "") {
1973
2596
  try {
1974
- const realCwd = realpathSync(cwd);
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: readFileSync2(full, "utf8") };
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 = readFileSync2(full);
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 = join5(realDir, safeName);
3355
+ const target = join7(realDir, safeName);
2733
3356
  if (relative2(realDir, dirname2(target)) !== "")
2734
3357
  return text("invalid filename", 400);
2735
- if (existsSync3(target))
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
- writeFileSync(fd, new Uint8Array(await upload.file.arrayBuffer()));
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
- unlinkSync(path);
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 = join5(homedir(), ".Trash");
3476
+ const trashDir = join7(homedir2(), ".Trash");
2854
3477
  const base = basename2(path) || "code-viewer-trash-item";
2855
- const target = join5(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
3478
+ const target = join7(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
2856
3479
  try {
2857
- mkdirSync(trashDir, { recursive: true });
2858
- renameSync(path, target);
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 (existsSync3(original))
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 (!existsSync3(trashPath))
3517
+ if (!existsSync5(trashPath))
2895
3518
  return { ok: false, error: "trash item not found" };
2896
3519
  try {
2897
- const trashRoot = join5(homedir(), ".Trash");
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
- mkdirSync(dirname2(original), { recursive: true });
2902
- renameSync(trashPath, original);
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 = join5(parent, name);
3048
- if (existsSync3(target))
3670
+ const target = join7(parent, name);
3671
+ if (existsSync5(target))
3049
3672
  return text("already exists", 409);
3050
3673
  try {
3051
- mkdirSync(target, { recursive: false });
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
- parseCli();
3110
- var server = await startServer({
3111
- hostname: "127.0.0.1",
3112
- port: listenPort,
3113
- async fetch(req) {
3114
- if (!requestAllowed(req))
3115
- return text("forbidden", 403);
3116
- const url = new URL(req.url);
3117
- const staticResponse = staticFile(url.pathname);
3118
- if (staticResponse)
3119
- return staticResponse;
3120
- if (url.pathname === "/diff.json")
3121
- return handleDiffJson(url);
3122
- if (url.pathname === "/_settings")
3123
- return handleSettings();
3124
- if (url.pathname === "/_tree")
3125
- return handleTree(url);
3126
- if (url.pathname === "/_files")
3127
- return handleFiles(url);
3128
- if (url.pathname === "/_grep")
3129
- return handleGrep(url);
3130
- if (url.pathname === "/_commits")
3131
- return handleRefCommits(url);
3132
- if (url.pathname === "/file_diff")
3133
- return handleFileDiff(url);
3134
- if (url.pathname === "/file_range")
3135
- return handleFileRange(url);
3136
- if (url.pathname === "/_file")
3137
- return handleRawFile(req, url);
3138
- if (url.pathname === "/_open_path")
3139
- return handleOpenPath(req);
3140
- if (url.pathname === "/_trash_path")
3141
- return handleTrashPath(req);
3142
- if (url.pathname === "/_restore_trash")
3143
- return handleRestoreTrash(req);
3144
- if (url.pathname === "/_create_directory")
3145
- return handleCreateDirectory(req);
3146
- if (url.pathname === "/_upload_files")
3147
- return handleUploadFiles(req);
3148
- if (url.pathname === "/_refs")
3149
- return json(refs(cwd));
3150
- if (url.pathname === "/refresh" && req.method === "POST") {
3151
- if (!sideEffectRequestAllowed(req))
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
- triggerUpdate();
3154
- return json({ ok: true, generation });
3155
- }
3156
- if (url.pathname === "/events") {
3157
- let ctrl;
3158
- let keepalive;
3159
- return new Response(new ReadableStream({
3160
- start(controller) {
3161
- ctrl = controller;
3162
- sseClients.add(controller);
3163
- controller.enqueue(enc.encode(`event: open
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
- keepalive = setInterval(() => {
3168
- try {
3169
- controller.enqueue(enc.encode(`: ping
3943
+ keepalive = setInterval(() => {
3944
+ try {
3945
+ controller.enqueue(enc.encode(`: ping
3170
3946
 
3171
3947
  `));
3172
- } catch {
3173
- sseClients.delete(controller);
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
- }, 15000);
3177
- },
3178
- cancel() {
3179
- if (ctrl)
3180
- sseClients.delete(ctrl);
3181
- if (keepalive)
3182
- clearInterval(keepalive);
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
- return text("not found", 404);
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
- if (openAfterStart) {
3195
- openBrowser(`http://127.0.0.1:${server.port}/`);
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}`);