@youtyan/code-viewer 0.1.32 → 0.1.34

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