@youtyan/code-viewer 0.8.2 → 0.8.4

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.
@@ -94,6 +94,157 @@ var init_sqlite_driver = __esm(() => {
94
94
  NPX_CACHE_GUIDE = 'Stale npx cache likely contains a binary compiled for a different Node.js version. Fix: `rm -rf ~/.npm/_npx` (macOS / Linux) or `Remove-Item -Recurse -Force "$(npm config get cache)\\_npx"` (Windows), ' + "then re-run `npx -y @youtyan/code-viewer@latest …`.";
95
95
  });
96
96
 
97
+ // web-src/core/id.ts
98
+ function bytesToHex(bytes) {
99
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
100
+ }
101
+ function bytesToBase36(bytes) {
102
+ return Array.from(bytes, (b) => BASE36_ALPHABET[b % BASE36_ALPHABET.length]).join("");
103
+ }
104
+ function randomBase36(length) {
105
+ const cryptoApi = globalThis.crypto;
106
+ if (typeof cryptoApi?.getRandomValues === "function") {
107
+ const bytes = new Uint8Array(length);
108
+ cryptoApi.getRandomValues(bytes);
109
+ return bytesToBase36(bytes);
110
+ }
111
+ return Math.random().toString(36).slice(2, 2 + length).padEnd(length, "0");
112
+ }
113
+ function makeId(prefix) {
114
+ const cryptoApi = globalThis.crypto;
115
+ if (typeof cryptoApi?.randomUUID === "function") {
116
+ return `${prefix}-${cryptoApi.randomUUID().replace(/-/g, "").slice(0, 16)}`;
117
+ }
118
+ if (typeof cryptoApi?.getRandomValues === "function") {
119
+ const bytes = new Uint8Array(8);
120
+ cryptoApi.getRandomValues(bytes);
121
+ return `${prefix}-${bytesToHex(bytes)}`;
122
+ }
123
+ return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
124
+ }
125
+ function makeTimedId(prefix) {
126
+ const time = Date.now().toString(36);
127
+ const random = randomBase36(6);
128
+ return `${prefix}-${time}${random}`;
129
+ }
130
+ var BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
131
+
132
+ // web-src/core/routes.ts
133
+ function assertNever(value) {
134
+ throw new Error(`unhandled route: ${JSON.stringify(value)}`);
135
+ }
136
+ function parseLineTarget(value) {
137
+ const raw = value || "";
138
+ const range = /^(\d+)-(\d+)$/.exec(raw);
139
+ if (range) {
140
+ const a = Number(range[1]);
141
+ const b = Number(range[2]);
142
+ const start = Math.min(a, b);
143
+ const end = Math.max(a, b);
144
+ if (start > 0)
145
+ return { start, end };
146
+ return;
147
+ }
148
+ const line = Number(raw);
149
+ return Number.isInteger(line) && line > 0 ? line : undefined;
150
+ }
151
+ function formatLineTarget(line) {
152
+ return typeof line === "number" ? String(line) : `${line.start}-${line.end}`;
153
+ }
154
+ function buildRoute(route) {
155
+ switch (route.screen) {
156
+ case "repo": {
157
+ const params = new URLSearchParams;
158
+ if (route.ref && route.ref !== "worktree")
159
+ params.set("ref", route.ref);
160
+ if (route.path)
161
+ params.set("path", route.path);
162
+ const qs = params.toString();
163
+ return `/${qs ? `?${qs}` : ""}`;
164
+ }
165
+ case "file":
166
+ if (route.view === "blob") {
167
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=blob" + (route.preview ? "&preview=1" : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
168
+ }
169
+ if (route.view === "blame") {
170
+ const ref = route.ref || "worktree";
171
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(ref) + "&view=blame" + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
172
+ }
173
+ if (route.view === "history") {
174
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=history" + (route.commit ? `&commit=${encodeURIComponent(route.commit)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
175
+ }
176
+ return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
177
+ case "diff":
178
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.path ? `&path=${encodeURIComponent(route.path)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
179
+ case "help": {
180
+ const params = new URLSearchParams;
181
+ if (route.lang && route.lang !== "en")
182
+ params.set("lang", route.lang);
183
+ if (route.section && route.section !== "overview")
184
+ params.set("section", route.section);
185
+ const qs = params.toString();
186
+ return `/help${qs ? `?${qs}` : ""}`;
187
+ }
188
+ case "history": {
189
+ const params = new URLSearchParams;
190
+ if (route.ref && route.ref !== "HEAD")
191
+ params.set("ref", route.ref);
192
+ if (route.commit)
193
+ params.set("commit", route.commit);
194
+ const qs = params.toString();
195
+ return `/history${qs ? `?${qs}` : ""}`;
196
+ }
197
+ case "journal": {
198
+ const params = new URLSearchParams;
199
+ if (route.tab && route.tab !== "journal")
200
+ params.set("tab", route.tab);
201
+ if (route.date)
202
+ params.set("date", route.date);
203
+ if (route.label)
204
+ params.set("label", route.label);
205
+ if (route.task)
206
+ params.set("task", route.task);
207
+ const qs = params.toString();
208
+ return `/journal${qs ? `?${qs}` : ""}`;
209
+ }
210
+ case "database": {
211
+ const params = new URLSearchParams;
212
+ if (route.db)
213
+ params.set("db", route.db);
214
+ if (route.schema)
215
+ params.set("schema", route.schema);
216
+ if (route.table)
217
+ params.set("table", route.table);
218
+ if (route.tab)
219
+ params.set("tab", route.tab);
220
+ if (route.diffBefore)
221
+ params.set("diffBefore", route.diffBefore);
222
+ if (route.diffAfter)
223
+ params.set("diffAfter", route.diffAfter);
224
+ const qs = params.toString();
225
+ return `/database${qs ? `?${qs}` : ""}`;
226
+ }
227
+ case "unknown":
228
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
229
+ default:
230
+ return assertNever(route);
231
+ }
232
+ }
233
+ var SPA_PATHS, APP_ENTRY_PATHS;
234
+ var init_routes = __esm(() => {
235
+ SPA_PATHS = [
236
+ "/todif",
237
+ "/todiff",
238
+ "/file",
239
+ "/help",
240
+ "/history",
241
+ "/journal",
242
+ "/database",
243
+ "/doctor"
244
+ ];
245
+ APP_ENTRY_PATHS = ["/", "/index.html"];
246
+ });
247
+
97
248
  // web-src/server/json-store.ts
98
249
  import { randomBytes } from "node:crypto";
99
250
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -205,6 +356,11 @@ function createJsonFileStore(options) {
205
356
  }
206
357
  var init_json_store = () => {};
207
358
 
359
+ // web-src/server/ordered-insert.ts
360
+ function orderedInsertOptionCount(input) {
361
+ return (input.before_id ? 1 : 0) + (input.after_id ? 1 : 0) + (input.position !== undefined ? 1 : 0);
362
+ }
363
+
208
364
  // web-src/server/annotations.ts
209
365
  import { join } from "node:path";
210
366
  function annotationsFilePath(root) {
@@ -214,9 +370,7 @@ function emptyAnnotationsState() {
214
370
  return { version: 1, sessions: [] };
215
371
  }
216
372
  function makeAnnotationId(prefix) {
217
- const random = Math.random().toString(36).slice(2, 8);
218
- const time = Date.now().toString(36);
219
- return `${prefix}-${time}${random}`;
373
+ return makeTimedId(prefix);
220
374
  }
221
375
  function normalizeLineRange(raw) {
222
376
  if (!raw || typeof raw !== "object")
@@ -229,16 +383,10 @@ function normalizeLineRange(raw) {
229
383
  return { start, end: endValue };
230
384
  }
231
385
  function parseAnnotationLine(raw) {
232
- const range = /^(\d+)-(\d+)$/.exec(raw);
233
- if (range) {
234
- const a = Number(range[1]);
235
- const b = Number(range[2]);
236
- const start = Math.min(a, b);
237
- const end = Math.max(a, b);
238
- return start > 0 ? { start, end } : undefined;
239
- }
240
- const line = Number(raw);
241
- return Number.isInteger(line) && line > 0 ? { start: line, end: line } : undefined;
386
+ const target = parseLineTarget(raw);
387
+ if (target === undefined)
388
+ return;
389
+ return typeof target === "number" ? { start: target, end: target } : target;
242
390
  }
243
391
  function normalizeRange(raw) {
244
392
  const from = raw && typeof raw === "object" && typeof raw.from === "string" ? raw.from || "HEAD" : "HEAD";
@@ -441,11 +589,8 @@ function startAnnotationSession(state, title, now, id = makeAnnotationId("s")) {
441
589
  session
442
590
  };
443
591
  }
444
- function insertOptionCount(input) {
445
- return (input.before_id ? 1 : 0) + (input.after_id ? 1 : 0) + (input.position !== undefined ? 1 : 0);
446
- }
447
592
  function entryInsertIndex(entries, input) {
448
- if (insertOptionCount(input) > 1)
593
+ if (orderedInsertOptionCount(input) > 1)
449
594
  return { ok: false, error: "use only one of before, after, or position" };
450
595
  if (input.before_id) {
451
596
  const index = entries.findIndex((entry) => entry.id === input.before_id);
@@ -471,7 +616,7 @@ function entryInsertIndex(entries, input) {
471
616
  function findSessionByEntryId(sessions, entryId) {
472
617
  return sessions.find((session) => session.entries.some((entry) => entry.id === entryId));
473
618
  }
474
- function addAnnotationEntry(state, input, now, makeId = makeAnnotationId) {
619
+ function addAnnotationEntry(state, input, now, makeId2 = makeAnnotationId) {
475
620
  const target = normalizeAnnotationTarget(input.target);
476
621
  if (target?.kind === "database" && !target.db)
477
622
  return { ok: false, error: "database annotation requires db" };
@@ -514,13 +659,13 @@ function addAnnotationEntry(state, input, now, makeId = makeAnnotationId) {
514
659
  session = sessions[sessions.length - 1];
515
660
  }
516
661
  if (!session) {
517
- const started = startAnnotationSession(state, input.session_title || "", now, makeId("s"));
662
+ const started = startAnnotationSession(state, input.session_title || "", now, makeId2("s"));
518
663
  sessions = started.state.sessions;
519
664
  session = started.session;
520
665
  createdSession = true;
521
666
  }
522
667
  const entry = {
523
- id: makeId("a"),
668
+ id: makeId2("a"),
524
669
  created_at: now,
525
670
  path,
526
671
  range: normalizeRange(input.range),
@@ -554,7 +699,7 @@ function addAnnotationEntry(state, input, now, makeId = makeAnnotationId) {
554
699
  };
555
700
  }
556
701
  function moveAnnotationEntry(state, id, input) {
557
- if (insertOptionCount(input) !== 1)
702
+ if (orderedInsertOptionCount(input) !== 1)
558
703
  return { ok: false, error: "move requires before, after, or position" };
559
704
  const sourceSession = findSessionByEntryId(state.sessions, id);
560
705
  const entry = sourceSession?.entries.find((e) => e.id === id);
@@ -654,6 +799,7 @@ function deleteAnnotationById(state, id) {
654
799
  }
655
800
  var CODE_VIEWER_DIR = ".code-viewer", ANNOTATIONS_FILE_NAME = "annotations.json", ANNOTATION_BODY_MAX_BYTES, ANNOTATION_TITLE_MAX_CHARS = 300, MAX_ANNOTATIONS_JSON_BYTES = 5000000, annotationsStore;
656
801
  var init_annotations = __esm(() => {
802
+ init_routes();
657
803
  init_json_store();
658
804
  ANNOTATION_BODY_MAX_BYTES = 64 * 1024;
659
805
  annotationsStore = createJsonFileStore({
@@ -835,6 +981,44 @@ var init_command_resolver = __esm(() => {
835
981
  activeOverrides = new Map;
836
982
  });
837
983
 
984
+ // web-src/server/cache.ts
985
+ import { lstatSync } from "node:fs";
986
+ import { join as join3 } from "node:path";
987
+ function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
988
+ return !!cached && now - cached.storedAt <= ttlMs;
989
+ }
990
+ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
991
+ cache.set(key, { ...value, storedAt: now });
992
+ while (cache.size > maxEntries) {
993
+ const oldest = cache.keys().next().value;
994
+ if (oldest === undefined)
995
+ break;
996
+ cache.delete(oldest);
997
+ }
998
+ }
999
+ function worktreeFileSignature(path, cwd) {
1000
+ try {
1001
+ const stats = lstatSync(join3(cwd, path));
1002
+ const inode = "ino" in stats ? stats.ino : 0;
1003
+ return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
1004
+ } catch {
1005
+ return "state:missing";
1006
+ }
1007
+ }
1008
+ function fileDiffCacheKey(options) {
1009
+ const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
1010
+ if (options.isUntracked && !worktreeTarget) {
1011
+ throw new Error("untracked file diffs require a worktree range");
1012
+ }
1013
+ const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
1014
+ if (options.isUntracked) {
1015
+ return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
1016
+ }
1017
+ return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
1018
+ }
1019
+ var CACHE_TTL_MS = 1500, MAX_TIMED_CACHE_ENTRIES = 200;
1020
+ var init_cache = () => {};
1021
+
838
1022
  // web-src/server/name-pattern.ts
839
1023
  function parseGlobSegment(pattern) {
840
1024
  const matchers = [];
@@ -947,6 +1131,21 @@ import {
947
1131
  } from "node:http";
948
1132
  import { Readable } from "node:stream";
949
1133
  function runSync(args, cwd, options = {}) {
1134
+ const proc = runBytesSync(args, cwd, options);
1135
+ return {
1136
+ code: proc.code,
1137
+ stdout: new TextDecoder().decode(proc.stdout),
1138
+ stderr: proc.stderr
1139
+ };
1140
+ }
1141
+ function runAsync(args, cwd, options = {}) {
1142
+ return runBytesAsync(args, cwd, options).then((proc) => ({
1143
+ code: proc.code,
1144
+ stdout: new TextDecoder().decode(proc.stdout),
1145
+ stderr: proc.stderr
1146
+ }));
1147
+ }
1148
+ function runBytesSync(args, cwd, options = {}) {
950
1149
  const proc = spawnSync(args[0], args.slice(1), {
951
1150
  cwd,
952
1151
  encoding: "buffer",
@@ -957,17 +1156,10 @@ function runSync(args, cwd, options = {}) {
957
1156
  });
958
1157
  return {
959
1158
  code: proc.status ?? (proc.error ? 1 : 0),
960
- stdout: new TextDecoder().decode(proc.stdout || new Uint8Array),
1159
+ stdout: new Uint8Array(proc.stdout || new Uint8Array),
961
1160
  stderr: appendProcessError(new TextDecoder().decode(proc.stderr || new Uint8Array), proc.error)
962
1161
  };
963
1162
  }
964
- function runAsync(args, cwd, options = {}) {
965
- return runBytesAsync(args, cwd, options).then((proc) => ({
966
- code: proc.code,
967
- stdout: new TextDecoder().decode(proc.stdout),
968
- stderr: proc.stderr
969
- }));
970
- }
971
1163
  function runBytesAsync(args, cwd, options = {}) {
972
1164
  const maxBuffer = options.maxBuffer ?? 64 * 1024 * 1024;
973
1165
  return new Promise((resolve) => {
@@ -1217,14 +1409,16 @@ var init_runtime = () => {};
1217
1409
  import {
1218
1410
  closeSync,
1219
1411
  existsSync,
1220
- lstatSync,
1412
+ lstatSync as lstatSync2,
1221
1413
  openSync,
1222
1414
  readdirSync,
1223
1415
  readFileSync,
1416
+ readlinkSync,
1224
1417
  readSync,
1418
+ realpathSync as realpathSync2,
1225
1419
  statSync as statSync2
1226
1420
  } from "node:fs";
1227
- import { join as join3 } from "node:path";
1421
+ import { dirname as dirname3, join as join4, posix, relative as relative2 } from "node:path";
1228
1422
  function normalizeBlameRef(ref, base) {
1229
1423
  const rawRef = ref || "worktree";
1230
1424
  if (base === "worktree" && rawRef !== "worktree") {
@@ -1322,12 +1516,76 @@ async function statusPorcelainForPathAsync(path, cwd) {
1322
1516
  error: gitFailureMessage(res, "git status failed")
1323
1517
  };
1324
1518
  }
1519
+ async function repoStatusMapAsync(cwd, now = Date.now()) {
1520
+ const cached = repoStatusMapCache.get(cwd);
1521
+ if (cacheFresh(cached, now))
1522
+ return cached.map;
1523
+ const map = new Map;
1524
+ const res = await runGitAsync([
1525
+ "git",
1526
+ "-c",
1527
+ "core.quotepath=false",
1528
+ "status",
1529
+ "--porcelain=v1",
1530
+ "-z",
1531
+ "--untracked-files=all"
1532
+ ], cwd);
1533
+ if (res.code !== 0)
1534
+ return map;
1535
+ const records = res.stdout.split("\x00").filter(Boolean);
1536
+ for (let i = 0;i < records.length; i++) {
1537
+ const record = records[i];
1538
+ const xy = record.slice(0, 2);
1539
+ const path = record.slice(3);
1540
+ if (!path)
1541
+ continue;
1542
+ if (xy === "??") {
1543
+ map.set(path, "A");
1544
+ continue;
1545
+ }
1546
+ if (xy[0] === "R" || xy[0] === "C" || xy[1] === "R" || xy[1] === "C") {
1547
+ i++;
1548
+ map.set(path, "R");
1549
+ continue;
1550
+ }
1551
+ const code = xy[0] !== " " ? xy[0] : xy[1];
1552
+ if (code && code !== " ")
1553
+ map.set(path, code);
1554
+ }
1555
+ setTimedCacheEntry(repoStatusMapCache, cwd, { map }, now);
1556
+ return map;
1557
+ }
1325
1558
  function show(ref, path, cwd) {
1326
1559
  return run(["git", "show", `${ref}:${path}`], cwd);
1327
1560
  }
1328
1561
  function showAsync(ref, path, cwd) {
1329
1562
  return runGitAsync(["git", "show", `${ref}:${path}`], cwd);
1330
1563
  }
1564
+ function resolveSymlinkPath(linkPath, target) {
1565
+ if (!target || target.startsWith("/") || target.includes("\x00"))
1566
+ return null;
1567
+ const baseDir = dirname3(linkPath);
1568
+ const combined = baseDir === "." ? target : `${baseDir}/${target}`;
1569
+ const normalized = posix.normalize(combined);
1570
+ if (normalized === "." || normalized === "")
1571
+ return "";
1572
+ if (normalized === ".." || normalized.startsWith("../"))
1573
+ return null;
1574
+ return normalized;
1575
+ }
1576
+ async function gitSymlinkTargetMetadataAsync(ref, path, cwd) {
1577
+ const res = await showAsync(ref, path, cwd);
1578
+ if (res.code !== 0)
1579
+ return { symlink_target_type: "missing" };
1580
+ const target = res.stdout;
1581
+ const resolved = resolveSymlinkPath(path, target);
1582
+ if (resolved === null)
1583
+ return { symlink_target: target, symlink_target_type: "missing" };
1584
+ const type = await runGitAsync(["git", "cat-file", "-t", `${ref}:${resolved}`], cwd);
1585
+ const kind = type.stdout.trim();
1586
+ const symlink_target_type = kind === "tree" ? "tree" : kind === "blob" ? "blob" : "missing";
1587
+ return symlink_target_type === "missing" ? { symlink_target: target, symlink_target_type } : { symlink_target: target, symlink_target_type, resolved_path: resolved };
1588
+ }
1331
1589
  function catFileBlobStream(oid, cwd) {
1332
1590
  return spawnStream(resolveGitArgs(["git", "cat-file", "blob", oid]), cwd);
1333
1591
  }
@@ -1958,7 +2216,7 @@ function isGitInternalPath(path) {
1958
2216
  return pathHasSegment(path, ".git");
1959
2217
  }
1960
2218
  function syntheticUncommittedBlameFromWorktree(cwd, path) {
1961
- const filePath = join3(cwd, path);
2219
+ const filePath = join4(cwd, path);
1962
2220
  try {
1963
2221
  const stat = statSync2(filePath);
1964
2222
  if (!stat.isFile())
@@ -2217,7 +2475,7 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
2217
2475
  return omitDirNames.matches(name) ? "heavy" : undefined;
2218
2476
  }
2219
2477
  function worktreeSubmodulePaths(cwd) {
2220
- if (!existsSync(join3(cwd, ".gitmodules")))
2478
+ if (!existsSync(join4(cwd, ".gitmodules")))
2221
2479
  return new Set;
2222
2480
  const res = run(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
2223
2481
  if (res.code !== 0)
@@ -2229,7 +2487,7 @@ function worktreeSubmodulePaths(cwd) {
2229
2487
  }).filter(Boolean));
2230
2488
  }
2231
2489
  async function worktreeSubmodulePathsAsync(cwd) {
2232
- if (!existsSync(join3(cwd, ".gitmodules")))
2490
+ if (!existsSync(join4(cwd, ".gitmodules")))
2233
2491
  return new Set;
2234
2492
  const res = await runGitAsync(["git", "config", "--file", ".gitmodules", "--get-regexp", "\\.path$"], cwd);
2235
2493
  if (res.code !== 0)
@@ -2240,7 +2498,48 @@ async function worktreeSubmodulePathsAsync(cwd) {
2240
2498
  return split >= 0 ? normalizeTreePath(line.slice(split + 1)) : "";
2241
2499
  }).filter(Boolean));
2242
2500
  }
2243
- function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames, submodulePaths) {
2501
+ function realpathWithinRepo(cwd, full, allowRoot) {
2502
+ try {
2503
+ const realCwd = realpathSync2(cwd);
2504
+ const realFull = realpathSync2(full);
2505
+ const rel = relative2(realCwd, realFull);
2506
+ if (rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
2507
+ return null;
2508
+ if (rel === "" && !allowRoot)
2509
+ return null;
2510
+ return realFull;
2511
+ } catch {
2512
+ return null;
2513
+ }
2514
+ }
2515
+ function resolveWorktreeSymlinkTarget(cwd, full) {
2516
+ let symlink_target;
2517
+ try {
2518
+ symlink_target = readlinkSync(full);
2519
+ } catch {
2520
+ symlink_target = undefined;
2521
+ }
2522
+ let symlink_target_type = "missing";
2523
+ if (realpathWithinRepo(cwd, full, false) !== null) {
2524
+ try {
2525
+ const stat = statSync2(full);
2526
+ symlink_target_type = stat.isDirectory() ? "tree" : stat.isFile() ? "blob" : "missing";
2527
+ } catch {
2528
+ symlink_target_type = "missing";
2529
+ }
2530
+ }
2531
+ return symlink_target === undefined ? { symlink_target_type } : { symlink_target, symlink_target_type };
2532
+ }
2533
+ function recursiveWorktreeFileEntry(cwd, full, name, path, isSymlink) {
2534
+ const symlinkInfo = isSymlink ? resolveWorktreeSymlinkTarget(cwd, full) : null;
2535
+ return {
2536
+ name,
2537
+ path,
2538
+ type: "blob",
2539
+ ...symlinkInfo ? { is_symlink: true, ...symlinkInfo } : {}
2540
+ };
2541
+ }
2542
+ function worktreeEntryFromDirent(cwd, base, dir, name, isDirectory, isSymlink, omitDirNames, excludeNames, submodulePaths) {
2244
2543
  if (excludeNames.matches(name))
2245
2544
  return {
2246
2545
  name,
@@ -2248,10 +2547,18 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
2248
2547
  type: isDirectory ? "tree" : "blob"
2249
2548
  };
2250
2549
  const entryPath = base ? `${base}/${name}` : name;
2251
- const type = isDirectory ? hasDotGitEntry(join3(dir, name)) ? "commit" : "tree" : "blob";
2550
+ const symlinkInfo = isSymlink ? resolveWorktreeSymlinkTarget(cwd, join4(dir, name)) : null;
2551
+ const resolvedIsDirectory = symlinkInfo ? symlinkInfo.symlink_target_type === "tree" : isDirectory;
2552
+ const type = symlinkInfo && symlinkInfo.symlink_target_type === "missing" ? "blob" : resolvedIsDirectory ? hasDotGitEntry(join4(dir, name)) ? "commit" : "tree" : "blob";
2252
2553
  const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
2253
2554
  const submodule = type === "commit" && submodulePaths.has(entryPath) ? true : undefined;
2254
- const baseEntry = submodule ? { name, path: entryPath, type, submodule } : { name, path: entryPath, type };
2555
+ const baseEntry = {
2556
+ name,
2557
+ path: entryPath,
2558
+ type,
2559
+ ...submodule ? { submodule } : {},
2560
+ ...symlinkInfo ? { is_symlink: true, ...symlinkInfo } : {}
2561
+ };
2255
2562
  return omittedReason ? {
2256
2563
  ...baseEntry,
2257
2564
  children_omitted: true,
@@ -2260,14 +2567,16 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
2260
2567
  }
2261
2568
  function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
2262
2569
  const base = normalizeTreePath(path);
2263
- const root = join3(cwd, base);
2570
+ const root = join4(cwd, base);
2571
+ if (realpathWithinRepo(cwd, root, true) === null)
2572
+ return [];
2264
2573
  const omitDirNameSet = compileNamePatterns(omitDirNames);
2265
2574
  const excludeNameSet = compileNamePatterns(excludeNames);
2266
2575
  const submodulePaths = worktreeSubmodulePaths(cwd);
2267
2576
  let directEntries;
2268
2577
  try {
2269
2578
  const dirents = readdirSync(root, { withFileTypes: true });
2270
- directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
2579
+ directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(cwd, base, root, entry.name, entry.isDirectory(), entry.isSymbolicLink(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
2271
2580
  } catch {
2272
2581
  return [];
2273
2582
  }
@@ -2307,7 +2616,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
2307
2616
  if (excludeNameSet.matches(entry.name))
2308
2617
  continue;
2309
2618
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2310
- const full = join3(dir, entry.name);
2619
+ const full = join4(dir, entry.name);
2311
2620
  if (entry.isDirectory()) {
2312
2621
  const omittedReason = omittedWorktreeDirectoryReason(entry.name, omitDirNameSet);
2313
2622
  if (omittedReason) {
@@ -2325,11 +2634,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
2325
2634
  continue;
2326
2635
  walk(full, entryPath, depth + 1);
2327
2636
  } else if (entry.isFile() || entry.isSymbolicLink()) {
2328
- if (!pushRecursiveEntry({
2329
- name: entry.name,
2330
- path: entryPath,
2331
- type: "blob"
2332
- }))
2637
+ if (!pushRecursiveEntry(recursiveWorktreeFileEntry(cwd, full, entry.name, entryPath, entry.isSymbolicLink())))
2333
2638
  return;
2334
2639
  }
2335
2640
  }
@@ -2339,14 +2644,16 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
2339
2644
  }
2340
2645
  async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
2341
2646
  const base = normalizeTreePath(path);
2342
- const root = join3(cwd, base);
2647
+ const root = join4(cwd, base);
2648
+ if (realpathWithinRepo(cwd, root, true) === null)
2649
+ return [];
2343
2650
  const omitDirNameSet = compileNamePatterns(omitDirNames);
2344
2651
  const excludeNameSet = compileNamePatterns(excludeNames);
2345
2652
  const submodulePaths = await worktreeSubmodulePathsAsync(cwd);
2346
2653
  let directEntries;
2347
2654
  try {
2348
2655
  const dirents = readdirSync(root, { withFileTypes: true });
2349
- directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
2656
+ directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(cwd, base, root, entry.name, entry.isDirectory(), entry.isSymbolicLink(), omitDirNameSet, excludeNameSet, submodulePaths)).filter((entry) => entry.path));
2350
2657
  } catch {
2351
2658
  return [];
2352
2659
  }
@@ -2394,7 +2701,7 @@ async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames
2394
2701
  if (excludeNameSet.matches(entry.name))
2395
2702
  continue;
2396
2703
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2397
- const full = join3(dir, entry.name);
2704
+ const full = join4(dir, entry.name);
2398
2705
  if (entry.isDirectory()) {
2399
2706
  const omittedReason = omittedWorktreeDirectoryReason(entry.name, omitDirNameSet);
2400
2707
  if (omittedReason) {
@@ -2412,11 +2719,7 @@ async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames
2412
2719
  continue;
2413
2720
  await walk(full, entryPath, depth + 1);
2414
2721
  } else if (entry.isFile() || entry.isSymbolicLink()) {
2415
- if (!pushRecursiveEntry({
2416
- name: entry.name,
2417
- path: entryPath,
2418
- type: "blob"
2419
- }))
2722
+ if (!pushRecursiveEntry(recursiveWorktreeFileEntry(cwd, full, entry.name, entryPath, entry.isSymbolicLink())))
2420
2723
  return;
2421
2724
  }
2422
2725
  }
@@ -2426,12 +2729,24 @@ async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames
2426
2729
  }
2427
2730
  function hasDotGitEntry(dir) {
2428
2731
  try {
2429
- lstatSync(join3(dir, ".git"));
2732
+ lstatSync2(join4(dir, ".git"));
2430
2733
  return true;
2431
2734
  } catch (err) {
2432
2735
  return !!err && typeof err === "object" && "code" in err && err.code !== "ENOENT";
2433
2736
  }
2434
2737
  }
2738
+ function parseLsTreeRecord(rec, allowedTypes) {
2739
+ const match = rec.match(new RegExp(`^(\\d+)\\s+(${allowedTypes})\\s+[0-9a-fA-F]+\\t(.+)$`));
2740
+ if (!match)
2741
+ return null;
2742
+ const [, mode, type, entryPath] = match;
2743
+ return {
2744
+ name: entryPath.split("/").pop() || entryPath,
2745
+ path: entryPath,
2746
+ type,
2747
+ ...mode === LS_TREE_SYMLINK_MODE ? { is_symlink: true } : {}
2748
+ };
2749
+ }
2435
2750
  function gitTreeEntries(ref, path, cwd, recursive) {
2436
2751
  const base = normalizeTreePath(path);
2437
2752
  const args = ["git", "-c", "core.quotepath=false", "ls-tree"];
@@ -2444,17 +2759,7 @@ function gitTreeEntries(ref, path, cwd, recursive) {
2444
2759
  if (res.code !== 0)
2445
2760
  return { code: res.code, entries: [], stderr: res.stderr };
2446
2761
  const allowedTypes = recursive ? "blob|commit" : "tree|blob|commit";
2447
- let entries = res.stdout.split("\x00").filter(Boolean).map((rec) => {
2448
- const match = rec.match(new RegExp(`^\\d+\\s+(${allowedTypes})\\s+[0-9a-fA-F]+\\t(.+)$`));
2449
- if (!match)
2450
- return null;
2451
- const entryPath = match[2];
2452
- return {
2453
- name: entryPath.split("/").pop() || entryPath,
2454
- path: entryPath,
2455
- type: match[1]
2456
- };
2457
- }).filter((entry) => !!entry);
2762
+ let entries = res.stdout.split("\x00").filter(Boolean).map((rec) => parseLsTreeRecord(rec, allowedTypes)).filter((entry) => !!entry);
2458
2763
  if (recursive)
2459
2764
  entries.sort((a, b) => a.path.localeCompare(b.path));
2460
2765
  else
@@ -2473,17 +2778,7 @@ async function gitTreeEntriesAsync(ref, path, cwd, recursive) {
2473
2778
  if (res.code !== 0)
2474
2779
  return { code: res.code, entries: [], stderr: res.stderr };
2475
2780
  const allowedTypes = recursive ? "blob|commit" : "tree|blob|commit";
2476
- let entries = res.stdout.split("\x00").filter(Boolean).map((rec) => {
2477
- const match = rec.match(new RegExp(`^\\d+\\s+(${allowedTypes})\\s+[0-9a-fA-F]+\\t(.+)$`));
2478
- if (!match)
2479
- return null;
2480
- const entryPath = match[2];
2481
- return {
2482
- name: entryPath.split("/").pop() || entryPath,
2483
- path: entryPath,
2484
- type: match[1]
2485
- };
2486
- }).filter((entry) => !!entry);
2781
+ let entries = res.stdout.split("\x00").filter(Boolean).map((rec) => parseLsTreeRecord(rec, allowedTypes)).filter((entry) => !!entry);
2487
2782
  if (recursive)
2488
2783
  entries.sort((a, b) => a.path.localeCompare(b.path));
2489
2784
  else
@@ -2547,7 +2842,7 @@ async function listTreeResultAsync(ref, path, cwd, options = {}) {
2547
2842
  }
2548
2843
  function untrackedMeta(cwd) {
2549
2844
  return untracked(cwd).flatMap((path) => {
2550
- const full = join3(cwd, path);
2845
+ const full = join4(cwd, path);
2551
2846
  let fileExists = false;
2552
2847
  try {
2553
2848
  fileExists = existsSync(full) && statSync2(full).isFile();
@@ -2579,7 +2874,7 @@ function untrackedMeta(cwd) {
2579
2874
  async function untrackedMetaAsync(cwd) {
2580
2875
  const paths = await untrackedAsync(cwd);
2581
2876
  return paths.flatMap((path) => {
2582
- const full = join3(cwd, path);
2877
+ const full = join4(cwd, path);
2583
2878
  let fileExists = false;
2584
2879
  try {
2585
2880
  fileExists = existsSync(full) && statSync2(full).isFile();
@@ -2827,8 +3122,9 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
2827
3122
  lineTruncated
2828
3123
  };
2829
3124
  }
2830
- var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", 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", ALWAYS_WORKTREE_OMIT_DIR_NAMES, DEFAULT_WORKTREE_OMIT_DIR_NAMES, GIT_COMMAND_TIMEOUT_MS = 20000, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
3125
+ var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", 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", ALWAYS_WORKTREE_OMIT_DIR_NAMES, DEFAULT_WORKTREE_OMIT_DIR_NAMES, GIT_COMMAND_TIMEOUT_MS = 20000, repoStatusMapCache, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200, LS_TREE_SYMLINK_MODE = "120000";
2831
3126
  var init_git = __esm(() => {
3127
+ init_cache();
2832
3128
  init_command_resolver();
2833
3129
  init_name_pattern();
2834
3130
  init_runtime();
@@ -2877,6 +3173,7 @@ var init_git = __esm(() => {
2877
3173
  "bin",
2878
3174
  "obj"
2879
3175
  ];
3176
+ repoStatusMapCache = new Map;
2880
3177
  });
2881
3178
 
2882
3179
  // web-src/server/server-registry.ts
@@ -2889,16 +3186,16 @@ import {
2889
3186
  writeFileSync
2890
3187
  } from "node:fs";
2891
3188
  import { homedir } from "node:os";
2892
- import { join as join4 } from "node:path";
3189
+ import { join as join5 } from "node:path";
2893
3190
  function registryDir() {
2894
3191
  const override = process.env.CODE_VIEWER_TEST_SERVER_REGISTRY_DIR;
2895
3192
  if (override)
2896
3193
  return override;
2897
- return join4(homedir(), ".cache", "code-viewer", "servers");
3194
+ return join5(homedir(), ".cache", "code-viewer", "servers");
2898
3195
  }
2899
3196
  function serverRegistryFilePath(root) {
2900
3197
  const hash = createHash("sha256").update(root).digest("hex").slice(0, 16);
2901
- return join4(registryDir(), `${hash}.json`);
3198
+ return join5(registryDir(), `${hash}.json`);
2902
3199
  }
2903
3200
  function writeServerRegistry(entry) {
2904
3201
  try {
@@ -2939,13 +3236,40 @@ function removeServerRegistry(root, pid) {
2939
3236
  var init_server_registry = () => {};
2940
3237
 
2941
3238
  // web-src/server/cli-helpers.ts
2942
- import { realpathSync as realpathSync2 } from "node:fs";
3239
+ import { realpathSync as realpathSync3 } from "node:fs";
2943
3240
  function takeValue(argv, index, flag) {
2944
3241
  const value = argv[index + 1];
2945
3242
  if (value === undefined)
2946
3243
  return { error: `${flag} requires a value` };
2947
3244
  return { value, next: index + 1 };
2948
3245
  }
3246
+ function takeGlobalCliOption(argv, index, options) {
3247
+ const flag = argv[index];
3248
+ if (flag === "--cwd" || flag === "--server" && options.allowServer) {
3249
+ const taken = takeValue(argv, index, flag);
3250
+ if ("error" in taken)
3251
+ return { kind: "error", error: taken.error };
3252
+ return {
3253
+ kind: flag === "--cwd" ? "cwd" : "server",
3254
+ value: taken.value,
3255
+ next: taken.next
3256
+ };
3257
+ }
3258
+ if (flag === "--bin" && options.allowedCommands) {
3259
+ const taken = takeValue(argv, index, flag);
3260
+ if ("error" in taken)
3261
+ return { kind: "error", error: taken.error };
3262
+ const parsed = parseExternalCommandOverride(taken.value, "--bin", options.allowedCommands);
3263
+ if (parsed.ok === false)
3264
+ return { kind: "error", error: parsed.error };
3265
+ return {
3266
+ kind: "command-override",
3267
+ override: parsed.override,
3268
+ next: taken.next
3269
+ };
3270
+ }
3271
+ return { kind: "unhandled" };
3272
+ }
2949
3273
  function shellSingleQuote(value) {
2950
3274
  return `'${value.replace(/'/g, "'\\''")}'`;
2951
3275
  }
@@ -2965,7 +3289,7 @@ function isUnsafeText(value) {
2965
3289
  return true;
2966
3290
  return false;
2967
3291
  }
2968
- function validateRefValue(value, flag) {
3292
+ function validateSafeCliValue(value, flag) {
2969
3293
  if (!value)
2970
3294
  return `${flag} requires a non-empty value`;
2971
3295
  if (isUnsafeText(value))
@@ -2974,13 +3298,16 @@ function validateRefValue(value, flag) {
2974
3298
  return `${flag} must not start with '-'`;
2975
3299
  return;
2976
3300
  }
3301
+ function validateRefValue(value, flag) {
3302
+ const error = validateSafeCliValue(value, flag);
3303
+ if (error)
3304
+ return error;
3305
+ return;
3306
+ }
2977
3307
  function validateRepoRelativePathValue(value, flag) {
2978
- if (!value)
2979
- return `${flag} requires a non-empty value`;
2980
- if (isUnsafeText(value))
2981
- return `${flag} must be single-line and must not contain NUL`;
2982
- if (value.startsWith("-"))
2983
- return `${flag} must not start with '-'`;
3308
+ const error = validateSafeCliValue(value, flag);
3309
+ if (error)
3310
+ return error;
2984
3311
  if (value.startsWith("/") || value.startsWith("\\"))
2985
3312
  return `${flag} must be repo-relative`;
2986
3313
  const parts = value.split(/[\\/]+/);
@@ -2998,7 +3325,7 @@ function resolveRepoRootSafe(cwdOption) {
2998
3325
  const base = cwdOption || process.cwd();
2999
3326
  let baseReal;
3000
3327
  try {
3001
- baseReal = realpathSync2(base);
3328
+ baseReal = realpathSync3(base);
3002
3329
  } catch {
3003
3330
  return {
3004
3331
  ok: false,
@@ -3094,6 +3421,7 @@ function extractErrorDetail(rawBody, isJson, status) {
3094
3421
  return trimmed;
3095
3422
  }
3096
3423
  var init_cli_helpers = __esm(() => {
3424
+ init_command_resolver();
3097
3425
  init_git();
3098
3426
  init_server_registry();
3099
3427
  });
@@ -3113,6 +3441,28 @@ function parsePosition(value) {
3113
3441
  const n = Number(value);
3114
3442
  return Number.isInteger(n) && n > 0 ? n : Number.NaN;
3115
3443
  }
3444
+ function parseAnnotationAddOptions(options) {
3445
+ const body = options.get("--body");
3446
+ const bodyFile = options.get("--body-file");
3447
+ if (body !== undefined && bodyFile !== undefined)
3448
+ return { ok: false, error: "use either --body or --body-file" };
3449
+ const position = parsePosition(options.get("--position"));
3450
+ if (Number.isNaN(position))
3451
+ return { ok: false, error: "--position must be a positive integer" };
3452
+ return {
3453
+ ok: true,
3454
+ options: {
3455
+ title: options.get("--title"),
3456
+ session: options.get("--session"),
3457
+ sessionTitle: options.get("--session-title"),
3458
+ body,
3459
+ bodyFile,
3460
+ before: options.get("--before"),
3461
+ after: options.get("--after"),
3462
+ position
3463
+ }
3464
+ };
3465
+ }
3116
3466
  function parseFilter(value) {
3117
3467
  const idx = value.indexOf("=");
3118
3468
  if (idx <= 0)
@@ -3167,15 +3517,15 @@ function parseAnnotateArgs(argv) {
3167
3517
  const arg = argv[i];
3168
3518
  if (arg === "--help" || arg === "-h")
3169
3519
  return { ok: true, args: { command: { kind: "help" } } };
3170
- if (arg === "--cwd" || arg === "--server") {
3171
- const taken = takeValue(argv, i, arg);
3172
- if ("error" in taken)
3173
- return { ok: false, error: taken.error };
3174
- if (arg === "--cwd")
3175
- cwd = taken.value;
3176
- else
3177
- server = taken.value;
3178
- i = taken.next;
3520
+ const global = takeGlobalCliOption(argv, i, { allowServer: true });
3521
+ if (global.kind === "error")
3522
+ return { ok: false, error: global.error };
3523
+ if (global.kind === "cwd") {
3524
+ cwd = global.value;
3525
+ i = global.next;
3526
+ } else if (global.kind === "server") {
3527
+ server = global.value;
3528
+ i = global.next;
3179
3529
  } else if (valueFlags.has(arg)) {
3180
3530
  const taken = takeValue(argv, i, arg);
3181
3531
  if ("error" in taken)
@@ -3220,13 +3570,9 @@ function parseAnnotateArgs(argv) {
3220
3570
  if (!line)
3221
3571
  return { ok: false, error: "--line must be <n> or <n>-<m>" };
3222
3572
  }
3223
- const body = options.get("--body");
3224
- const bodyFile = options.get("--body-file");
3225
- if (body !== undefined && bodyFile !== undefined)
3226
- return { ok: false, error: "use either --body or --body-file" };
3227
- const position = parsePosition(options.get("--position"));
3228
- if (Number.isNaN(position))
3229
- return { ok: false, error: "--position must be a positive integer" };
3573
+ const commonOptions = parseAnnotationAddOptions(options);
3574
+ if (commonOptions.ok === false)
3575
+ return { ok: false, error: commonOptions.error };
3230
3576
  return {
3231
3577
  ok: true,
3232
3578
  args: {
@@ -3236,14 +3582,7 @@ function parseAnnotateArgs(argv) {
3236
3582
  line,
3237
3583
  from: options.get("--from"),
3238
3584
  to: options.get("--to"),
3239
- title: options.get("--title"),
3240
- session: options.get("--session"),
3241
- sessionTitle: options.get("--session-title"),
3242
- body,
3243
- bodyFile,
3244
- before: options.get("--before"),
3245
- after: options.get("--after"),
3246
- position
3585
+ ...commonOptions.options
3247
3586
  },
3248
3587
  cwd,
3249
3588
  server
@@ -3253,13 +3592,9 @@ function parseAnnotateArgs(argv) {
3253
3592
  if (subcommand === "add-db") {
3254
3593
  if (!options.get("--db"))
3255
3594
  return { ok: false, error: "add-db requires --db <id>" };
3256
- const body = options.get("--body");
3257
- const bodyFile = options.get("--body-file");
3258
- if (body !== undefined && bodyFile !== undefined)
3259
- return { ok: false, error: "use either --body or --body-file" };
3260
- const position = parsePosition(options.get("--position"));
3261
- if (Number.isNaN(position))
3262
- return { ok: false, error: "--position must be a positive integer" };
3595
+ const commonOptions = parseAnnotationAddOptions(options);
3596
+ if (commonOptions.ok === false)
3597
+ return { ok: false, error: commonOptions.error };
3263
3598
  const rawTab = options.get("--tab");
3264
3599
  const tab = normalizeDatabaseTab(rawTab);
3265
3600
  if (rawTab !== undefined && tab === undefined)
@@ -3334,14 +3669,7 @@ function parseAnnotateArgs(argv) {
3334
3669
  searchTerm: options.get("--search-term"),
3335
3670
  includeNonText: flags.has("--include-non-text") || undefined,
3336
3671
  searchAutoRun: flags.has("--run-search"),
3337
- title: options.get("--title"),
3338
- session: options.get("--session"),
3339
- sessionTitle: options.get("--session-title"),
3340
- body,
3341
- bodyFile,
3342
- before: options.get("--before"),
3343
- after: options.get("--after"),
3344
- position
3672
+ ...commonOptions.options
3345
3673
  },
3346
3674
  cwd,
3347
3675
  server
@@ -3454,6 +3782,14 @@ function printList(state) {
3454
3782
  });
3455
3783
  }
3456
3784
  }
3785
+ function printAddedAnnotation(result, location, serverUrl) {
3786
+ const sessionTitle = result.session_title || "Untitled session";
3787
+ if (result.created_session) {
3788
+ console.error(`created new annotation session ${result.session_id} (${sessionTitle})`);
3789
+ }
3790
+ console.log(`annotated ${location} ` + `[${result.entry.id}] in session ${result.session_id} (${sessionTitle})`);
3791
+ console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
3792
+ }
3457
3793
  async function annotationBodyFromCommand(command) {
3458
3794
  let body = command.body;
3459
3795
  if (body === undefined && command.bodyFile !== undefined) {
@@ -3514,11 +3850,7 @@ async function runAnnotateCli(argv) {
3514
3850
  after_id: command.after,
3515
3851
  position: command.position
3516
3852
  });
3517
- if (result.created_session) {
3518
- console.error(`created new annotation session ${result.session_id} (${result.session_title || "Untitled session"})`);
3519
- }
3520
- console.log(`annotated ${result.entry.path}${formatLine(result.entry.line)} ` + `[${result.entry.id}] in session ${result.session_id} (${result.session_title || "Untitled session"})`);
3521
- console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
3853
+ printAddedAnnotation(result, `${result.entry.path}${formatLine(result.entry.line)}`, serverUrl);
3522
3854
  return;
3523
3855
  }
3524
3856
  if (command.kind === "add-db") {
@@ -3568,11 +3900,7 @@ async function runAnnotateCli(argv) {
3568
3900
  after_id: command.after,
3569
3901
  position: command.position
3570
3902
  });
3571
- if (result.created_session) {
3572
- console.error(`created new annotation session ${result.session_id} (${result.session_title || "Untitled session"})`);
3573
- }
3574
- console.log(`annotated ${result.entry.path} ` + `[${result.entry.id}] in session ${result.session_id} (${result.session_title || "Untitled session"})`);
3575
- console.error(`view annotations at ${serverUrl}/ with the code annotations panel`);
3903
+ printAddedAnnotation(result, result.entry.path, serverUrl);
3576
3904
  return;
3577
3905
  }
3578
3906
  if (command.kind === "list") {
@@ -4108,8 +4436,8 @@ __export(exports_file_cli, {
4108
4436
  FILE_DEFAULT_HISTORY_LIMIT: () => FILE_DEFAULT_HISTORY_LIMIT,
4109
4437
  FILE_AGENT_HELP: () => FILE_AGENT_HELP
4110
4438
  });
4111
- import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
4112
- import { join as join5, relative as relative2 } from "node:path";
4439
+ import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync as realpathSync4, statSync as statSync3 } from "node:fs";
4440
+ import { join as join6, relative as relative3 } from "node:path";
4113
4441
  function validatePath(value) {
4114
4442
  return validateRepoRelativePathValue(value, "--path");
4115
4443
  }
@@ -4133,23 +4461,17 @@ function parseFileArgs(argv) {
4133
4461
  if (arg === "--help" || arg === "-h") {
4134
4462
  return { ok: true, args: { command: { kind: "help" } } };
4135
4463
  }
4136
- if (arg === "--cwd") {
4137
- const taken = takeValue(argv, i, arg);
4138
- if ("error" in taken)
4139
- return { ok: false, error: taken.error };
4140
- cwd = taken.value;
4141
- i = taken.next;
4142
- } else if (arg === "--bin") {
4143
- const taken = takeValue(argv, i, arg);
4144
- if ("error" in taken)
4145
- return { ok: false, error: taken.error };
4146
- const parsed = parseExternalCommandOverride(taken.value, "--bin", [
4147
- "git"
4148
- ]);
4149
- if (parsed.ok === false)
4150
- return { ok: false, error: parsed.error };
4151
- commandOverrides.push(parsed.override);
4152
- i = taken.next;
4464
+ const global = takeGlobalCliOption(argv, i, {
4465
+ allowedCommands: ["git"]
4466
+ });
4467
+ if (global.kind === "error")
4468
+ return { ok: false, error: global.error };
4469
+ if (global.kind === "cwd") {
4470
+ cwd = global.value;
4471
+ i = global.next;
4472
+ } else if (global.kind === "command-override") {
4473
+ commandOverrides.push(global.override);
4474
+ i = global.next;
4153
4475
  } else if (VALUE_FLAGS.has(arg)) {
4154
4476
  const taken = takeValue(argv, i, arg);
4155
4477
  if ("error" in taken)
@@ -4513,13 +4835,13 @@ function sliceLines(text, start, end) {
4513
4835
  function safeWorktreePathFromRoot(root, path) {
4514
4836
  if (validatePath(path))
4515
4837
  return null;
4516
- const full = join5(root, path);
4838
+ const full = join6(root, path);
4517
4839
  if (!existsSync3(full))
4518
4840
  return null;
4519
4841
  try {
4520
- const realRoot = realpathSync3(root);
4521
- const realFull = realpathSync3(full);
4522
- const rel = relative2(realRoot, realFull);
4842
+ const realRoot = realpathSync4(root);
4843
+ const realFull = realpathSync4(full);
4844
+ const rel = relative3(realRoot, realFull);
4523
4845
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\")) {
4524
4846
  return null;
4525
4847
  }
@@ -5391,26 +5713,21 @@ function parseJournalArgs(argv) {
5391
5713
  const arg = argv[i];
5392
5714
  if (arg === "--help" || arg === "-h")
5393
5715
  return { ok: true, args: { command: { kind: "help" }, dryRun: false } };
5394
- if (arg === "--cwd" || arg === "--server") {
5395
- const taken = takeValue(argv, i, arg);
5396
- if ("error" in taken)
5397
- return { ok: false, error: taken.error };
5398
- if (arg === "--cwd")
5399
- cwd = taken.value;
5400
- else
5401
- server = taken.value;
5402
- i = taken.next;
5403
- } else if (arg === "--bin") {
5404
- const taken = takeValue(argv, i, arg);
5405
- if ("error" in taken)
5406
- return { ok: false, error: taken.error };
5407
- const parsed = parseExternalCommandOverride(taken.value, "--bin", [
5408
- "gh"
5409
- ]);
5410
- if (parsed.ok === false)
5411
- return { ok: false, error: parsed.error };
5412
- commandOverrides.push(parsed.override);
5413
- i = taken.next;
5716
+ const global = takeGlobalCliOption(argv, i, {
5717
+ allowServer: true,
5718
+ allowedCommands: ["gh"]
5719
+ });
5720
+ if (global.kind === "error")
5721
+ return { ok: false, error: global.error };
5722
+ if (global.kind === "cwd") {
5723
+ cwd = global.value;
5724
+ i = global.next;
5725
+ } else if (global.kind === "server") {
5726
+ server = global.value;
5727
+ i = global.next;
5728
+ } else if (global.kind === "command-override") {
5729
+ commandOverrides.push(global.override);
5730
+ i = global.next;
5414
5731
  } else if (valueFlags.has(arg)) {
5415
5732
  const taken = takeValue(argv, i, arg);
5416
5733
  if ("error" in taken)
@@ -6138,162 +6455,61 @@ Usage:
6138
6455
  [--priority <p0|p1|p2|p3>] [--label <label>...] [--clear-labels]
6139
6456
  [--due <YYYY-MM-DD|none>] [--source-date <YYYY-MM-DD|today|none>]
6140
6457
  [--body <markdown> | --body-file <path>]
6141
- code-viewer journal task-next [--label <label>...] [--status <status>]
6142
- [--limit <n>] [--json]
6143
- code-viewer journal github-issues [--repo <owner/repo>]
6144
- [--state <open|closed|all>] [--gh-label <label>...]
6145
- [--search <query>] [--limit <n>] [--json] [--bin gh=<path>]
6146
- code-viewer journal task-link-issue <number> [--repo <owner/repo>]
6147
- [--status <status>] [--priority <p0|p1|p2|p3>] [--label <label>...]
6148
- [--before <id> | --after <id> | --position <n>] [--json]
6149
- [--dry-run] [--bin gh=<path>]
6150
- code-viewer journal task-claim <id> [--by <agent>] [--lease-minutes <n>]
6151
- [--wip-limit <n>]
6152
- code-viewer journal task-done <id> --by <agent>
6153
- [--note <markdown> | --note-file <path>]
6154
- code-viewer journal task-delete <id>
6155
-
6156
- Global options:
6157
- --cwd <dir> repository root (default: current directory)
6158
- --server <url> code-viewer server URL (default: auto-discovered)
6159
- --bin gh=<p> override gh executable path for github-issues
6160
- --dry-run print the write payload without sending it
6161
- `, JOURNAL_AGENT_HELP = `code-viewer journal — agent guide
6162
-
6163
- You are an AI coding agent. Use this tool to write daily work notes and process
6164
- explicit task queues without guessing from memory.
6165
-
6166
- ## Workflow
6167
-
6168
- 1. Check the queue before doing label-scoped work:
6169
- code-viewer journal task-next --label ai-ready --limit 5 --json
6170
- 2. Claim exactly one task before editing code:
6171
- code-viewer journal task-claim <task-id> --by agent --wip-limit 1
6172
- 3. When finished, mark it done with a short note:
6173
- code-viewer journal task-done <task-id> --by agent --note "Implemented and verified."
6174
- 4. Add a daily journal entry for work that is not already represented by a task:
6175
- code-viewer journal add --date today --label ai --body "..."
6176
- 5. Inspect GitHub issues read-only before deciding what local task to create:
6177
- code-viewer journal github-issues --repo owner/repo --json
6178
- 6. Link a GitHub issue to the local board without updating GitHub:
6179
- code-viewer journal task-link-issue 123 --repo owner/repo --status draft --label ai-ready
6180
-
6181
- ## Rules
6182
-
6183
- - Treat labels as filters, priority as importance, and card order as human order.
6184
- - Do not process draft tasks unless the human asked for drafts.
6185
- - Use task-next for ordering; do not sort the JSON yourself unless asked.
6186
- - Use --dry-run before large generated entries.
6187
- - GitHub issue listing is read-only. Create or update local tasks explicitly.
6188
- - task-link-issue reads issue metadata only and stores a local task link plus
6189
- your local labels. It does not copy the issue body or update GitHub.
6190
- `;
6191
- var init_journal_cli = __esm(() => {
6192
- init_journal();
6193
- init_cli_helpers();
6194
- init_command_resolver();
6195
- init_github_issues();
6196
- });
6197
-
6198
- // web-src/core/routes.ts
6199
- function assertNever(value) {
6200
- throw new Error(`unhandled route: ${JSON.stringify(value)}`);
6201
- }
6202
- function formatLineTarget(line) {
6203
- return typeof line === "number" ? String(line) : `${line.start}-${line.end}`;
6204
- }
6205
- function buildRoute(route) {
6206
- switch (route.screen) {
6207
- case "repo": {
6208
- const params = new URLSearchParams;
6209
- if (route.ref && route.ref !== "worktree")
6210
- params.set("ref", route.ref);
6211
- if (route.path)
6212
- params.set("path", route.path);
6213
- const qs = params.toString();
6214
- return `/${qs ? `?${qs}` : ""}`;
6215
- }
6216
- case "file":
6217
- if (route.view === "blob") {
6218
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=blob" + (route.preview ? "&preview=1" : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
6219
- }
6220
- if (route.view === "blame") {
6221
- const ref = route.ref || "worktree";
6222
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(ref) + "&view=blame" + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
6223
- }
6224
- if (route.view === "history") {
6225
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=history" + (route.commit ? `&commit=${encodeURIComponent(route.commit)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
6226
- }
6227
- return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
6228
- case "diff":
6229
- return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.path ? `&path=${encodeURIComponent(route.path)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
6230
- case "help": {
6231
- const params = new URLSearchParams;
6232
- if (route.lang && route.lang !== "en")
6233
- params.set("lang", route.lang);
6234
- if (route.section && route.section !== "overview")
6235
- params.set("section", route.section);
6236
- const qs = params.toString();
6237
- return `/help${qs ? `?${qs}` : ""}`;
6238
- }
6239
- case "history": {
6240
- const params = new URLSearchParams;
6241
- if (route.ref && route.ref !== "HEAD")
6242
- params.set("ref", route.ref);
6243
- if (route.commit)
6244
- params.set("commit", route.commit);
6245
- const qs = params.toString();
6246
- return `/history${qs ? `?${qs}` : ""}`;
6247
- }
6248
- case "journal": {
6249
- const params = new URLSearchParams;
6250
- if (route.tab && route.tab !== "journal")
6251
- params.set("tab", route.tab);
6252
- if (route.date)
6253
- params.set("date", route.date);
6254
- if (route.label)
6255
- params.set("label", route.label);
6256
- if (route.task)
6257
- params.set("task", route.task);
6258
- const qs = params.toString();
6259
- return `/journal${qs ? `?${qs}` : ""}`;
6260
- }
6261
- case "database": {
6262
- const params = new URLSearchParams;
6263
- if (route.db)
6264
- params.set("db", route.db);
6265
- if (route.schema)
6266
- params.set("schema", route.schema);
6267
- if (route.table)
6268
- params.set("table", route.table);
6269
- if (route.tab)
6270
- params.set("tab", route.tab);
6271
- if (route.diffBefore)
6272
- params.set("diffBefore", route.diffBefore);
6273
- if (route.diffAfter)
6274
- params.set("diffAfter", route.diffAfter);
6275
- const qs = params.toString();
6276
- return `/database${qs ? `?${qs}` : ""}`;
6277
- }
6278
- case "unknown":
6279
- return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
6280
- default:
6281
- return assertNever(route);
6282
- }
6283
- }
6284
- var SPA_PATHS, APP_ENTRY_PATHS;
6285
- var init_routes = __esm(() => {
6286
- SPA_PATHS = [
6287
- "/todif",
6288
- "/todiff",
6289
- "/file",
6290
- "/help",
6291
- "/history",
6292
- "/journal",
6293
- "/database",
6294
- "/doctor"
6295
- ];
6296
- APP_ENTRY_PATHS = ["/", "/index.html"];
6458
+ code-viewer journal task-next [--label <label>...] [--status <status>]
6459
+ [--limit <n>] [--json]
6460
+ code-viewer journal github-issues [--repo <owner/repo>]
6461
+ [--state <open|closed|all>] [--gh-label <label>...]
6462
+ [--search <query>] [--limit <n>] [--json] [--bin gh=<path>]
6463
+ code-viewer journal task-link-issue <number> [--repo <owner/repo>]
6464
+ [--status <status>] [--priority <p0|p1|p2|p3>] [--label <label>...]
6465
+ [--before <id> | --after <id> | --position <n>] [--json]
6466
+ [--dry-run] [--bin gh=<path>]
6467
+ code-viewer journal task-claim <id> [--by <agent>] [--lease-minutes <n>]
6468
+ [--wip-limit <n>]
6469
+ code-viewer journal task-done <id> --by <agent>
6470
+ [--note <markdown> | --note-file <path>]
6471
+ code-viewer journal task-delete <id>
6472
+
6473
+ Global options:
6474
+ --cwd <dir> repository root (default: current directory)
6475
+ --server <url> code-viewer server URL (default: auto-discovered)
6476
+ --bin gh=<p> override gh executable path for github-issues
6477
+ --dry-run print the write payload without sending it
6478
+ `, JOURNAL_AGENT_HELP = `code-viewer journal — agent guide
6479
+
6480
+ You are an AI coding agent. Use this tool to write daily work notes and process
6481
+ explicit task queues without guessing from memory.
6482
+
6483
+ ## Workflow
6484
+
6485
+ 1. Check the queue before doing label-scoped work:
6486
+ code-viewer journal task-next --label ai-ready --limit 5 --json
6487
+ 2. Claim exactly one task before editing code:
6488
+ code-viewer journal task-claim <task-id> --by agent --wip-limit 1
6489
+ 3. When finished, mark it done with a short note:
6490
+ code-viewer journal task-done <task-id> --by agent --note "Implemented and verified."
6491
+ 4. Add a daily journal entry for work that is not already represented by a task:
6492
+ code-viewer journal add --date today --label ai --body "..."
6493
+ 5. Inspect GitHub issues read-only before deciding what local task to create:
6494
+ code-viewer journal github-issues --repo owner/repo --json
6495
+ 6. Link a GitHub issue to the local board without updating GitHub:
6496
+ code-viewer journal task-link-issue 123 --repo owner/repo --status draft --label ai-ready
6497
+
6498
+ ## Rules
6499
+
6500
+ - Treat labels as filters, priority as importance, and card order as human order.
6501
+ - Do not process draft tasks unless the human asked for drafts.
6502
+ - Use task-next for ordering; do not sort the JSON yourself unless asked.
6503
+ - Use --dry-run before large generated entries.
6504
+ - GitHub issue listing is read-only. Create or update local tasks explicitly.
6505
+ - task-link-issue reads issue metadata only and stores a local task link plus
6506
+ your local labels. It does not copy the issue body or update GitHub.
6507
+ `;
6508
+ var init_journal_cli = __esm(() => {
6509
+ init_journal();
6510
+ init_cli_helpers();
6511
+ init_command_resolver();
6512
+ init_github_issues();
6297
6513
  });
6298
6514
 
6299
6515
  // web-src/server/query-cli.ts
@@ -6338,26 +6554,21 @@ function parseQueryArgs(argv) {
6338
6554
  const arg = argv[i];
6339
6555
  if (arg === "--help" || arg === "-h")
6340
6556
  return { ok: true, args: { command: { kind: "help" } } };
6341
- if (arg === "--cwd" || arg === "--server") {
6342
- const taken = takeValue(argv, i, arg);
6343
- if ("error" in taken)
6344
- return { ok: false, error: taken.error };
6345
- if (arg === "--cwd")
6346
- cwd = taken.value;
6347
- else
6348
- server = taken.value;
6349
- i = taken.next;
6350
- } else if (arg === "--bin") {
6351
- const taken = takeValue(argv, i, arg);
6352
- if ("error" in taken)
6353
- return { ok: false, error: taken.error };
6354
- const parsed = parseExternalCommandOverride(taken.value, "--bin", [
6355
- "git"
6356
- ]);
6357
- if (parsed.ok === false)
6358
- return { ok: false, error: parsed.error };
6359
- commandOverrides.push(parsed.override);
6360
- i = taken.next;
6557
+ const global = takeGlobalCliOption(argv, i, {
6558
+ allowServer: true,
6559
+ allowedCommands: ["git"]
6560
+ });
6561
+ if (global.kind === "error")
6562
+ return { ok: false, error: global.error };
6563
+ if (global.kind === "cwd") {
6564
+ cwd = global.value;
6565
+ i = global.next;
6566
+ } else if (global.kind === "server") {
6567
+ server = global.value;
6568
+ i = global.next;
6569
+ } else if (global.kind === "command-override") {
6570
+ commandOverrides.push(global.override);
6571
+ i = global.next;
6361
6572
  } else if (VALUE_FLAGS2.has(arg)) {
6362
6573
  const taken = takeValue(argv, i, arg);
6363
6574
  if ("error" in taken)
@@ -8996,6 +9207,18 @@ __export(exports_search_cli, {
8996
9207
  SEARCH_AGENT_HELP: () => SEARCH_AGENT_HELP,
8997
9208
  FILE_NAME_SEARCH_DEFAULT_MAX: () => FILE_NAME_SEARCH_DEFAULT_MAX
8998
9209
  });
9210
+ function parseSearchMax(raw, hardCap) {
9211
+ if (raw === undefined)
9212
+ return { value: undefined };
9213
+ const value = Number(raw);
9214
+ if (!Number.isInteger(value) || value <= 0) {
9215
+ return { error: `--max must be a positive integer (got ${raw})` };
9216
+ }
9217
+ if (value > hardCap) {
9218
+ return { error: `--max must be <= ${hardCap} (got ${value})` };
9219
+ }
9220
+ return { value };
9221
+ }
8999
9222
  function parseSearchArgs(argv) {
9000
9223
  const rest = [];
9001
9224
  let cwd;
@@ -9009,26 +9232,21 @@ function parseSearchArgs(argv) {
9009
9232
  if (arg === "--help" || arg === "-h") {
9010
9233
  return { ok: true, args: { command: { kind: "help" } } };
9011
9234
  }
9012
- if (arg === "--cwd" || arg === "--server") {
9013
- const taken = takeValue(argv, i, arg);
9014
- if ("error" in taken)
9015
- return { ok: false, error: taken.error };
9016
- if (arg === "--cwd")
9017
- cwd = taken.value;
9018
- else
9019
- server = taken.value;
9020
- i = taken.next;
9021
- } else if (arg === "--bin") {
9022
- const taken = takeValue(argv, i, arg);
9023
- if ("error" in taken)
9024
- return { ok: false, error: taken.error };
9025
- const parsed = parseExternalCommandOverride(taken.value, "--bin", [
9026
- "git"
9027
- ]);
9028
- if (parsed.ok === false)
9029
- return { ok: false, error: parsed.error };
9030
- commandOverrides.push(parsed.override);
9031
- i = taken.next;
9235
+ const global = takeGlobalCliOption(argv, i, {
9236
+ allowServer: true,
9237
+ allowedCommands: ["git"]
9238
+ });
9239
+ if (global.kind === "error")
9240
+ return { ok: false, error: global.error };
9241
+ if (global.kind === "cwd") {
9242
+ cwd = global.value;
9243
+ i = global.next;
9244
+ } else if (global.kind === "server") {
9245
+ server = global.value;
9246
+ i = global.next;
9247
+ } else if (global.kind === "command-override") {
9248
+ commandOverrides.push(global.override);
9249
+ i = global.next;
9032
9250
  } else if (REPEATABLE_VALUE_FLAGS.has(arg)) {
9033
9251
  const taken = takeValue(argv, i, arg);
9034
9252
  if ("error" in taken)
@@ -9087,24 +9305,10 @@ function parseSearchArgs(argv) {
9087
9305
  error: "search files does not accept --path"
9088
9306
  };
9089
9307
  }
9090
- const maxRaw2 = options.get("--max");
9091
- let max2 = FILE_NAME_SEARCH_DEFAULT_MAX;
9092
- if (maxRaw2 !== undefined) {
9093
- const n = Number(maxRaw2);
9094
- if (!Number.isInteger(n) || n <= 0) {
9095
- return {
9096
- ok: false,
9097
- error: `--max must be a positive integer (got ${maxRaw2})`
9098
- };
9099
- }
9100
- if (n > FILE_SEARCH_ABSOLUTE_MAX) {
9101
- return {
9102
- ok: false,
9103
- error: `--max must be <= ${FILE_SEARCH_ABSOLUTE_MAX} (got ${n})`
9104
- };
9105
- }
9106
- max2 = n;
9107
- }
9308
+ const parsedMax2 = parseSearchMax(options.get("--max"), FILE_SEARCH_ABSOLUTE_MAX);
9309
+ if ("error" in parsedMax2)
9310
+ return { ok: false, error: parsedMax2.error };
9311
+ const max = parsedMax2.value ?? FILE_NAME_SEARCH_DEFAULT_MAX;
9108
9312
  return {
9109
9313
  ok: true,
9110
9314
  args: {
@@ -9112,7 +9316,7 @@ function parseSearchArgs(argv) {
9112
9316
  kind: "files",
9113
9317
  term,
9114
9318
  ref: options.get("--ref"),
9115
- max: max2,
9319
+ max,
9116
9320
  json: flags.has("--json")
9117
9321
  },
9118
9322
  cwd,
@@ -9121,24 +9325,9 @@ function parseSearchArgs(argv) {
9121
9325
  }
9122
9326
  };
9123
9327
  }
9124
- const maxRaw = options.get("--max");
9125
- let max;
9126
- if (maxRaw !== undefined) {
9127
- const n = Number(maxRaw);
9128
- if (!Number.isInteger(n) || n <= 0) {
9129
- return {
9130
- ok: false,
9131
- error: `--max must be a positive integer (got ${maxRaw})`
9132
- };
9133
- }
9134
- if (n > GREP_ABSOLUTE_MAX) {
9135
- return {
9136
- ok: false,
9137
- error: `--max must be <= ${GREP_ABSOLUTE_MAX} (got ${n})`
9138
- };
9139
- }
9140
- max = n;
9141
- }
9328
+ const parsedMax = parseSearchMax(options.get("--max"), GREP_ABSOLUTE_MAX);
9329
+ if ("error" in parsedMax)
9330
+ return { ok: false, error: parsedMax.error };
9142
9331
  return {
9143
9332
  ok: true,
9144
9333
  args: {
@@ -9148,7 +9337,7 @@ function parseSearchArgs(argv) {
9148
9337
  ref: options.get("--ref"),
9149
9338
  paths,
9150
9339
  regex: flags.has("--regex"),
9151
- max,
9340
+ max: parsedMax.value,
9152
9341
  json: flags.has("--json")
9153
9342
  },
9154
9343
  cwd,
@@ -9423,24 +9612,24 @@ Parse failures and unreachable servers exit 1.
9423
9612
 
9424
9613
  // web-src/server/root.ts
9425
9614
  import { existsSync as existsSync4 } from "node:fs";
9426
- import { dirname as dirname3, join as join6, normalize } from "node:path";
9615
+ import { dirname as dirname4, join as join7, normalize } from "node:path";
9427
9616
  import { fileURLToPath } from "node:url";
9428
9617
  function findRoot(start) {
9429
9618
  let current = start;
9430
9619
  for (let i = 0;i < 5; i++) {
9431
- if (existsSync4(join6(current, "package.json")) && existsSync4(join6(current, "web"))) {
9620
+ if (existsSync4(join7(current, "package.json")) && existsSync4(join7(current, "web"))) {
9432
9621
  return normalize(current);
9433
9622
  }
9434
- const parent = dirname3(current);
9623
+ const parent = dirname4(current);
9435
9624
  if (parent === current)
9436
9625
  break;
9437
9626
  current = parent;
9438
9627
  }
9439
- return normalize(join6(start, "..", ".."));
9628
+ return normalize(join7(start, "..", ".."));
9440
9629
  }
9441
9630
  var ROOT;
9442
9631
  var init_root = __esm(() => {
9443
- ROOT = findRoot(dirname3(fileURLToPath(import.meta.url)));
9632
+ ROOT = findRoot(dirname4(fileURLToPath(import.meta.url)));
9444
9633
  });
9445
9634
 
9446
9635
  // web-src/server/skill-cli.ts
@@ -9455,7 +9644,7 @@ __export(exports_skill_cli, {
9455
9644
  });
9456
9645
  import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
9457
9646
  import { homedir as homedir2 } from "node:os";
9458
- import { join as join7, resolve } from "node:path";
9647
+ import { join as join8, resolve } from "node:path";
9459
9648
  function parseAgentList(value) {
9460
9649
  if (value === "all")
9461
9650
  return [...AGENT_NAMES];
@@ -9515,7 +9704,7 @@ function parseSkillArgs(argv) {
9515
9704
  function discoverBundledSkills(skillsRoot) {
9516
9705
  if (!existsSync5(skillsRoot))
9517
9706
  return [];
9518
- return readdirSync2(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync5(join7(skillsRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
9707
+ return readdirSync2(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync5(join8(skillsRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
9519
9708
  }
9520
9709
  function installSkill(args, deps) {
9521
9710
  const skills = discoverBundledSkills(deps.skillsRoot);
@@ -9529,8 +9718,8 @@ function installSkill(args, deps) {
9529
9718
  const results = [];
9530
9719
  for (const agent of args.agents) {
9531
9720
  for (const skill of skills) {
9532
- const sourceDir = join7(deps.skillsRoot, skill);
9533
- const target = join7(base, AGENT_SKILL_DIRS[agent], "skills", skill);
9721
+ const sourceDir = join8(deps.skillsRoot, skill);
9722
+ const target = join8(base, AGENT_SKILL_DIRS[agent], "skills", skill);
9534
9723
  const action = existsSync5(target) ? "updated" : "installed";
9535
9724
  try {
9536
9725
  mkdirSync2(target, { recursive: true });
@@ -9559,7 +9748,7 @@ function runSkillCli(argv) {
9559
9748
  return;
9560
9749
  }
9561
9750
  const result = installSkill(parsed.args, {
9562
- skillsRoot: join7(ROOT, "skills"),
9751
+ skillsRoot: join8(ROOT, "skills"),
9563
9752
  homeDir: homedir2(),
9564
9753
  projectDir: process.cwd()
9565
9754
  });
@@ -9700,23 +9889,17 @@ function parseStatusArgs(argv) {
9700
9889
  if (arg === "--help" || arg === "-h") {
9701
9890
  return { ok: true, args: { command: { kind: "help" } } };
9702
9891
  }
9703
- if (arg === "--cwd") {
9704
- const taken = takeValue(argv, i, arg);
9705
- if ("error" in taken)
9706
- return { ok: false, error: taken.error };
9707
- cwd = taken.value;
9708
- i = taken.next;
9709
- } else if (arg === "--bin") {
9710
- const taken = takeValue(argv, i, arg);
9711
- if ("error" in taken)
9712
- return { ok: false, error: taken.error };
9713
- const parsed = parseExternalCommandOverride(taken.value, "--bin", [
9714
- "git"
9715
- ]);
9716
- if (parsed.ok === false)
9717
- return { ok: false, error: parsed.error };
9718
- commandOverrides.push(parsed.override);
9719
- i = taken.next;
9892
+ const global = takeGlobalCliOption(argv, i, {
9893
+ allowedCommands: ["git"]
9894
+ });
9895
+ if (global.kind === "error")
9896
+ return { ok: false, error: global.error };
9897
+ if (global.kind === "cwd") {
9898
+ cwd = global.value;
9899
+ i = global.next;
9900
+ } else if (global.kind === "command-override") {
9901
+ commandOverrides.push(global.override);
9902
+ i = global.next;
9720
9903
  } else if (VALUE_FLAGS4.has(arg)) {
9721
9904
  const taken = takeValue(argv, i, arg);
9722
9905
  if ("error" in taken)
@@ -10278,6 +10461,9 @@ function placeValue(coerced, kind, useParams, params) {
10278
10461
  function coerceCell(cell, columnType) {
10279
10462
  return coerceDbValue(cell.value, columnType);
10280
10463
  }
10464
+ function formatWriteComparisons(cells, columnTypes, kind, useParams, params, separator) {
10465
+ return cells.map((cell) => `${sanitizeIdentifier(cell.column, kind)} = ${placeValue(coerceCell(cell, columnTypes.get(cell.column) ?? "TEXT"), kind, useParams, params)}`).join(separator);
10466
+ }
10281
10467
  function buildInsertSql(table, cells, columnTypes, kind) {
10282
10468
  if (cells.length === 0) {
10283
10469
  throw new Error("insert requires at least one column value");
@@ -10298,8 +10484,8 @@ function buildUpdateSql(table, set, pk, columnTypes, kind) {
10298
10484
  }
10299
10485
  const useParams = useParamsFor(kind);
10300
10486
  const params = [];
10301
- const setSql = set.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(", ");
10302
- const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
10487
+ const setSql = formatWriteComparisons(set, columnTypes, kind, useParams, params, ", ");
10488
+ const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
10303
10489
  const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
10304
10490
  return { sql, params };
10305
10491
  }
@@ -10309,7 +10495,7 @@ function buildDeleteSql(table, pk, columnTypes, kind) {
10309
10495
  }
10310
10496
  const useParams = useParamsFor(kind);
10311
10497
  const params = [];
10312
- const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
10498
+ const whereSql = formatWriteComparisons(pk, columnTypes, kind, useParams, params, " AND ");
10313
10499
  const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
10314
10500
  return { sql, params };
10315
10501
  }
@@ -14623,11 +14809,11 @@ import {
14623
14809
  existsSync as existsSync6,
14624
14810
  openSync as openSync2,
14625
14811
  readSync as readSync2,
14626
- realpathSync as realpathSync4,
14812
+ realpathSync as realpathSync5,
14627
14813
  statSync as statSync4
14628
14814
  } from "node:fs";
14629
14815
  import { lstat, open, readdir, readFile as readFile2, stat } from "node:fs/promises";
14630
- import { basename, join as join8, relative as relative3 } from "node:path";
14816
+ import { basename, join as join9, relative as relative4 } from "node:path";
14631
14817
  function isSqliteFile(fullPath) {
14632
14818
  try {
14633
14819
  const stat2 = statSync4(fullPath);
@@ -14698,7 +14884,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
14698
14884
  return;
14699
14885
  if (omitSet.has(entry.toLowerCase()))
14700
14886
  continue;
14701
- const full = join8(dir, entry);
14887
+ const full = join9(dir, entry);
14702
14888
  let entryStat;
14703
14889
  try {
14704
14890
  entryStat = await lstat(full);
@@ -14715,7 +14901,7 @@ async function discoverSqliteFilesAsync(cwd, omitDirNames, signal) {
14715
14901
  continue;
14716
14902
  if (!await isSqliteFileAsync(full))
14717
14903
  continue;
14718
- const rel = relative3(cwd, full);
14904
+ const rel = relative4(cwd, full);
14719
14905
  if (rel.startsWith("..") || rel.startsWith("/"))
14720
14906
  continue;
14721
14907
  results.push({
@@ -14742,18 +14928,18 @@ function validateDbPath(cwd, dbPath) {
14742
14928
  const parts = dbPath.split(/[\\/]+/);
14743
14929
  if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
14744
14930
  return null;
14745
- const full = join8(cwd, dbPath);
14931
+ const full = join9(cwd, dbPath);
14746
14932
  if (!existsSync6(full))
14747
14933
  return null;
14748
14934
  let realCwd;
14749
14935
  let realFull;
14750
14936
  try {
14751
- realCwd = realpathSync4(cwd);
14752
- realFull = realpathSync4(full);
14937
+ realCwd = realpathSync5(cwd);
14938
+ realFull = realpathSync5(full);
14753
14939
  } catch {
14754
14940
  return null;
14755
14941
  }
14756
- const rel = relative3(realCwd, realFull);
14942
+ const rel = relative4(realCwd, realFull);
14757
14943
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/"))
14758
14944
  return null;
14759
14945
  if (!isSqliteFile(realFull))
@@ -14912,7 +15098,7 @@ function resolveEnvValue(raw, composeDirEnv = {}) {
14912
15098
  }
14913
15099
  async function readDotenvAsync(composeDir) {
14914
15100
  try {
14915
- const content = await readFile2(join8(composeDir, ".env"), "utf-8");
15101
+ const content = await readFile2(join9(composeDir, ".env"), "utf-8");
14916
15102
  return parseDotenvContent(content);
14917
15103
  } catch {
14918
15104
  return {};
@@ -15034,7 +15220,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
15034
15220
  for (let match = serviceRegex.exec(servicesBlock);match !== null; match = serviceRegex.exec(servicesBlock)) {
15035
15221
  servicePositions.push({ name: match[1], start: match.index });
15036
15222
  }
15037
- const relDir = relative3(cwd, composeDir);
15223
+ const relDir = relative4(cwd, composeDir);
15038
15224
  const isRoot = relDir === "" || relDir === ".";
15039
15225
  const relDirSlash = relDir.replace(/\\/g, "/");
15040
15226
  const filename = basename(filepath);
@@ -15139,7 +15325,7 @@ async function walkForMarkerFileAsync(dir, depth, omitSet, hasCapacity, visitDir
15139
15325
  return;
15140
15326
  if (omitSet.has(entry.toLowerCase()))
15141
15327
  continue;
15142
- const full = join8(dir, entry);
15328
+ const full = join9(dir, entry);
15143
15329
  let entryStat;
15144
15330
  try {
15145
15331
  entryStat = await lstat(full);
@@ -15166,7 +15352,7 @@ async function discoverDockerDatabasesAsync(cwd, omitDirNames = [], signal) {
15166
15352
  omitSet.add("node_modules");
15167
15353
  await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_DOCKER_SERVICES, async (dir) => {
15168
15354
  for (const filename of COMPOSE_FILENAMES) {
15169
- const filepath = join8(dir, filename);
15355
+ const filepath = join9(dir, filename);
15170
15356
  if (await pathExistsAsync(filepath)) {
15171
15357
  await parseComposeFileAsync(filepath, dir, cwd, results);
15172
15358
  break;
@@ -15330,7 +15516,7 @@ async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal)
15330
15516
  omitSet.add("node_modules");
15331
15517
  const results = [];
15332
15518
  await walkForMarkerFileAsync(cwd, 0, omitSet, () => results.length < MAX_SUPABASE_PROJECTS, async (dir) => {
15333
- const configPath = join8(dir, "supabase", "config.toml");
15519
+ const configPath = join9(dir, "supabase", "config.toml");
15334
15520
  if (!await pathExistsAsync(configPath))
15335
15521
  return;
15336
15522
  try {
@@ -15338,7 +15524,7 @@ async function discoverSupabaseCliProjectsAsync(cwd, omitDirNames = [], signal)
15338
15524
  const parsed = parseSupabaseConfigToml(content);
15339
15525
  if (!parsed)
15340
15526
  return;
15341
- const relDir = relative3(cwd, dir);
15527
+ const relDir = relative4(cwd, dir);
15342
15528
  const isRoot = relDir === "" || relDir === ".";
15343
15529
  const relDirSlash = relDir.replace(/\\/g, "/");
15344
15530
  const id = isRoot ? `supabase:${parsed.projectId}` : `supabase:${parsed.projectId}@${encodeURIComponent(relDirSlash)}`;
@@ -15420,35 +15606,18 @@ var init_discovery = __esm(() => {
15420
15606
  supabaseDiscoveryCache = new Map;
15421
15607
  });
15422
15608
 
15423
- // web-src/core/id.ts
15424
- function bytesToHex(bytes) {
15425
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
15426
- }
15427
- function makeId(prefix) {
15428
- const cryptoApi = globalThis.crypto;
15429
- if (typeof cryptoApi?.randomUUID === "function") {
15430
- return `${prefix}-${cryptoApi.randomUUID().replace(/-/g, "").slice(0, 16)}`;
15431
- }
15432
- if (typeof cryptoApi?.getRandomValues === "function") {
15433
- const bytes = new Uint8Array(8);
15434
- cryptoApi.getRandomValues(bytes);
15435
- return `${prefix}-${bytesToHex(bytes)}`;
15436
- }
15437
- return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
15438
- }
15439
-
15440
15609
  // web-src/server/worktree-watcher.ts
15441
15610
  import {
15442
- lstatSync as lstatSync2,
15611
+ lstatSync as lstatSync3,
15443
15612
  readdirSync as nodeReaddirSync,
15444
15613
  watch as nodeWatch
15445
15614
  } from "node:fs";
15446
- import { join as join9, relative as relative4 } from "node:path";
15615
+ import { join as join10, relative as relative5 } from "node:path";
15447
15616
  function normalizeRelativePath(path) {
15448
15617
  return path.replace(/\\/g, "/").replace(/^\/+/, "");
15449
15618
  }
15450
15619
  function isInsideRoot(root, path) {
15451
- const rel = relative4(root, path).replace(/\\/g, "/");
15620
+ const rel = relative5(root, path).replace(/\\/g, "/");
15452
15621
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
15453
15622
  }
15454
15623
  function startWorktreeUpdateWatch(options) {
@@ -15456,14 +15625,14 @@ function startWorktreeUpdateWatch(options) {
15456
15625
  const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
15457
15626
  const isDirectory = options.isDirectory || ((path) => {
15458
15627
  try {
15459
- return lstatSync2(path).isDirectory();
15628
+ return lstatSync3(path).isDirectory();
15460
15629
  } catch {
15461
15630
  return false;
15462
15631
  }
15463
15632
  });
15464
15633
  const directorySignature = options.directorySignature || ((path) => {
15465
15634
  try {
15466
- const stats = lstatSync2(path);
15635
+ const stats = lstatSync3(path);
15467
15636
  if (!stats.isDirectory())
15468
15637
  return null;
15469
15638
  return `${stats.dev}:${stats.ino}`;
@@ -15486,7 +15655,7 @@ function startWorktreeUpdateWatch(options) {
15486
15655
  const pendingChangedPaths = new Set;
15487
15656
  let watchLimitReported = false;
15488
15657
  const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
15489
- const directoryRelativePath = (dir) => normalizeRelativePath(relative4(options.root, dir));
15658
+ const directoryRelativePath = (dir) => normalizeRelativePath(relative5(options.root, dir));
15490
15659
  const ignoredDirectory = (dir) => {
15491
15660
  const rel = directoryRelativePath(dir);
15492
15661
  return Boolean(rel && ignored(rel));
@@ -15552,7 +15721,7 @@ function startWorktreeUpdateWatch(options) {
15552
15721
  for (const entry of entries) {
15553
15722
  if (!entry.isDirectory())
15554
15723
  continue;
15555
- const child = join9(dir, entry.name);
15724
+ const child = join10(dir, entry.name);
15556
15725
  if (ignoredDirectory(child))
15557
15726
  continue;
15558
15727
  children.push(child);
@@ -15636,10 +15805,10 @@ function startWorktreeUpdateWatch(options) {
15636
15805
  scheduleUpdate();
15637
15806
  return;
15638
15807
  }
15639
- const changed = normalizeRelativePath(join9(rel, filename.toString()));
15808
+ const changed = normalizeRelativePath(join10(rel, filename.toString()));
15640
15809
  if (ignored(changed))
15641
15810
  return;
15642
- const fullChangedPath = join9(options.root, changed);
15811
+ const fullChangedPath = join10(options.root, changed);
15643
15812
  if (!isInsideRoot(options.root, fullChangedPath))
15644
15813
  return;
15645
15814
  if (initialScanAsync) {
@@ -15688,9 +15857,9 @@ var init_worktree_watcher = __esm(() => {
15688
15857
  });
15689
15858
 
15690
15859
  // web-src/server/state-store.ts
15691
- import { join as join10 } from "node:path";
15860
+ import { join as join11 } from "node:path";
15692
15861
  function codeViewerPath(root, fileName) {
15693
- return join10(root, CODE_VIEWER_DIR2, fileName);
15862
+ return join11(root, CODE_VIEWER_DIR2, fileName);
15694
15863
  }
15695
15864
  function isRecord(value) {
15696
15865
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -15707,15 +15876,17 @@ function optionalString(value, maxLen) {
15707
15876
  function optionalBoolean(value) {
15708
15877
  return typeof value === "boolean" ? value : undefined;
15709
15878
  }
15710
- function optionalNumber(value, min, max) {
15879
+ function optionalFiniteNumber(value, min, max, round) {
15711
15880
  if (typeof value !== "number" || !Number.isFinite(value))
15712
15881
  return;
15713
- return Math.max(min, Math.min(max, Math.round(value)));
15882
+ const normalized = round ? Math.round(value) : value;
15883
+ return Math.max(min, Math.min(max, normalized));
15884
+ }
15885
+ function optionalNumber(value, min, max) {
15886
+ return optionalFiniteNumber(value, min, max, true);
15714
15887
  }
15715
15888
  function optionalFloat(value, min, max) {
15716
- if (typeof value !== "number" || !Number.isFinite(value))
15717
- return;
15718
- return Math.max(min, Math.min(max, value));
15889
+ return optionalFiniteNumber(value, min, max, false);
15719
15890
  }
15720
15891
  function optionalFontSize(value) {
15721
15892
  return value === "compact" || value === "regular" || value === "large" || value === "xlarge" ? value : undefined;
@@ -16547,6 +16718,25 @@ function asStringArray(value) {
16547
16718
  function asNumber(value) {
16548
16719
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
16549
16720
  }
16721
+ function asDynamoDbItemsResult(raw) {
16722
+ return {
16723
+ items: Array.isArray(raw.Items) ? raw.Items : [],
16724
+ count: asNumber(raw.Count),
16725
+ scannedCount: asNumber(raw.ScannedCount),
16726
+ ...raw.LastEvaluatedKey ? { lastEvaluatedKey: asObject(raw.LastEvaluatedKey) } : {}
16727
+ };
16728
+ }
16729
+ function dynamoDbItemsRequestFields(opts) {
16730
+ return {
16731
+ ...opts.limit ? { Limit: Math.min(1000, Math.max(1, opts.limit)) } : {},
16732
+ ...opts.exclusiveStartKey ? { ExclusiveStartKey: opts.exclusiveStartKey } : {},
16733
+ ...opts.indexName ? { IndexName: opts.indexName } : {},
16734
+ ...opts.projectionExpression ? { ProjectionExpression: opts.projectionExpression } : {},
16735
+ ...opts.filterExpression ? { FilterExpression: opts.filterExpression } : {},
16736
+ ...opts.expressionAttributeNames ? { ExpressionAttributeNames: opts.expressionAttributeNames } : {},
16737
+ ...opts.expressionAttributeValues ? { ExpressionAttributeValues: opts.expressionAttributeValues } : {}
16738
+ };
16739
+ }
16550
16740
  function createDynamoDbAdapter(config) {
16551
16741
  async function signedJsonRequest(action, body, signal, deadline = createDynamoDbTransportDeadline(config)) {
16552
16742
  const requestBody = JSON.stringify(body);
@@ -16629,41 +16819,19 @@ function createDynamoDbAdapter(config) {
16629
16819
  assertTableName(opts.tableName);
16630
16820
  const raw = await signedJsonRequest("Scan", {
16631
16821
  TableName: opts.tableName,
16632
- ...opts.limit ? { Limit: Math.min(1000, Math.max(1, opts.limit)) } : {},
16633
- ...opts.exclusiveStartKey ? { ExclusiveStartKey: opts.exclusiveStartKey } : {},
16634
- ...opts.indexName ? { IndexName: opts.indexName } : {},
16635
- ...opts.projectionExpression ? { ProjectionExpression: opts.projectionExpression } : {},
16636
- ...opts.filterExpression ? { FilterExpression: opts.filterExpression } : {},
16637
- ...opts.expressionAttributeNames ? { ExpressionAttributeNames: opts.expressionAttributeNames } : {},
16638
- ...opts.expressionAttributeValues ? { ExpressionAttributeValues: opts.expressionAttributeValues } : {}
16822
+ ...dynamoDbItemsRequestFields(opts)
16639
16823
  }, opts.signal);
16640
- return {
16641
- items: Array.isArray(raw.Items) ? raw.Items : [],
16642
- count: asNumber(raw.Count),
16643
- scannedCount: asNumber(raw.ScannedCount),
16644
- ...raw.LastEvaluatedKey ? { lastEvaluatedKey: asObject(raw.LastEvaluatedKey) } : {}
16645
- };
16824
+ return asDynamoDbItemsResult(raw);
16646
16825
  }
16647
16826
  async function queryAsync(opts) {
16648
16827
  assertTableName(opts.tableName);
16649
16828
  const raw = await signedJsonRequest("Query", {
16650
16829
  TableName: opts.tableName,
16651
16830
  KeyConditionExpression: opts.keyConditionExpression,
16652
- ...opts.limit ? { Limit: Math.min(1000, Math.max(1, opts.limit)) } : {},
16653
- ...opts.exclusiveStartKey ? { ExclusiveStartKey: opts.exclusiveStartKey } : {},
16654
- ...opts.indexName ? { IndexName: opts.indexName } : {},
16655
- ...opts.projectionExpression ? { ProjectionExpression: opts.projectionExpression } : {},
16656
- ...opts.filterExpression ? { FilterExpression: opts.filterExpression } : {},
16657
- ...opts.expressionAttributeNames ? { ExpressionAttributeNames: opts.expressionAttributeNames } : {},
16658
- ...opts.expressionAttributeValues ? { ExpressionAttributeValues: opts.expressionAttributeValues } : {},
16831
+ ...dynamoDbItemsRequestFields(opts),
16659
16832
  ...opts.scanIndexForward !== undefined ? { ScanIndexForward: opts.scanIndexForward } : {}
16660
16833
  }, opts.signal);
16661
- return {
16662
- items: Array.isArray(raw.Items) ? raw.Items : [],
16663
- count: asNumber(raw.Count),
16664
- scannedCount: asNumber(raw.ScannedCount),
16665
- ...raw.LastEvaluatedKey ? { lastEvaluatedKey: asObject(raw.LastEvaluatedKey) } : {}
16666
- };
16834
+ return asDynamoDbItemsResult(raw);
16667
16835
  }
16668
16836
  async function getItemAsync(opts) {
16669
16837
  assertTableName(opts.tableName);
@@ -16806,7 +16974,7 @@ var init_connection_pool = __esm(() => {
16806
16974
  // web-src/server/database/connections-store.ts
16807
16975
  import { randomUUID } from "node:crypto";
16808
16976
  import { chmod } from "node:fs/promises";
16809
- import { join as join11 } from "node:path";
16977
+ import { join as join12 } from "node:path";
16810
16978
  function secretKey(cwd, id) {
16811
16979
  return `${cwd}\x00${id}`;
16812
16980
  }
@@ -16827,7 +16995,7 @@ function withRuntimeSecrets(cwd, connection) {
16827
16995
  };
16828
16996
  }
16829
16997
  function connectionsFilePath(root) {
16830
- return join11(root, ".code-viewer", CONNECTIONS_FILE_NAME);
16998
+ return join12(root, ".code-viewer", CONNECTIONS_FILE_NAME);
16831
16999
  }
16832
17000
  function emptyState() {
16833
17001
  return { version: 1, connections: [] };
@@ -18609,9 +18777,9 @@ var init_handle_s3 = __esm(() => {
18609
18777
  });
18610
18778
 
18611
18779
  // web-src/server/database/query-history.ts
18612
- import { join as join12 } from "node:path";
18780
+ import { join as join13 } from "node:path";
18613
18781
  function historyFilePath(root) {
18614
- return join12(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
18782
+ return join13(root, CODE_VIEWER_DIR3, HISTORY_FILE_NAME);
18615
18783
  }
18616
18784
  function emptyState2() {
18617
18785
  return { version: 1, entries: [] };
@@ -18761,9 +18929,9 @@ var init_query_history = __esm(() => {
18761
18929
  // web-src/server/database/snapshot-store.ts
18762
18930
  import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
18763
18931
  import { mkdirSync as mkdirSync3 } from "node:fs";
18764
- import { join as join13 } from "node:path";
18932
+ import { join as join14 } from "node:path";
18765
18933
  async function getStoreDb(cwd) {
18766
- const dbPath = join13(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
18934
+ const dbPath = join14(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
18767
18935
  if (storeDb && storeDbPath === dbPath)
18768
18936
  return storeDb;
18769
18937
  if (storeDb) {
@@ -18771,7 +18939,7 @@ async function getStoreDb(cwd) {
18771
18939
  storeDb.close();
18772
18940
  } catch {}
18773
18941
  }
18774
- mkdirSync3(join13(cwd, CODE_VIEWER_DIR4), { recursive: true });
18942
+ mkdirSync3(join14(cwd, CODE_VIEWER_DIR4), { recursive: true });
18775
18943
  const DbClass = await loadSqliteClass();
18776
18944
  storeDb = new DbClass(dbPath);
18777
18945
  storeDbPath = dbPath;
@@ -19377,9 +19545,9 @@ var init_snapshot_runner = __esm(() => {
19377
19545
  });
19378
19546
 
19379
19547
  // web-src/server/database/tabs-store.ts
19380
- import { join as join14 } from "node:path";
19548
+ import { join as join15 } from "node:path";
19381
19549
  function tabsFilePath(root) {
19382
- return join14(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19550
+ return join15(root, CODE_VIEWER_DIR5, TABS_FILE_NAME);
19383
19551
  }
19384
19552
  function emptyState3() {
19385
19553
  return { version: 1, tabs: [], activeTabId: null };
@@ -21316,7 +21484,7 @@ var init_handle = __esm(() => {
21316
21484
 
21317
21485
  // web-src/server/doctor.ts
21318
21486
  import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
21319
- import { dirname as dirname4, join as join15, relative as relative5 } from "node:path";
21487
+ import { dirname as dirname5, join as join16, relative as relative6 } from "node:path";
21320
21488
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21321
21489
  function statusWorse(a, b) {
21322
21490
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -21405,12 +21573,12 @@ function findCodeViewerPackageJson() {
21405
21573
  try {
21406
21574
  let cursor;
21407
21575
  try {
21408
- cursor = dirname4(fileURLToPath2(import.meta.url));
21576
+ cursor = dirname5(fileURLToPath2(import.meta.url));
21409
21577
  } catch {
21410
- cursor = dirname4(process.argv[1] || ".");
21578
+ cursor = dirname5(process.argv[1] || ".");
21411
21579
  }
21412
21580
  for (let depth = 0;depth < 8; depth += 1) {
21413
- const candidate = join15(cursor, "package.json");
21581
+ const candidate = join16(cursor, "package.json");
21414
21582
  try {
21415
21583
  const raw = readFileSync6(candidate, "utf8");
21416
21584
  const pkg = JSON.parse(raw);
@@ -21418,7 +21586,7 @@ function findCodeViewerPackageJson() {
21418
21586
  return { version: pkg.version, path: candidate };
21419
21587
  }
21420
21588
  } catch {}
21421
- const next = dirname4(cursor);
21589
+ const next = dirname5(cursor);
21422
21590
  if (next === cursor)
21423
21591
  break;
21424
21592
  cursor = next;
@@ -21506,7 +21674,7 @@ async function checkSqlite(cwd) {
21506
21674
  return { id: "sqlite", title: "SQLite driver", rows };
21507
21675
  }
21508
21676
  async function trySnapshotDbOpen(cwd) {
21509
- const dbPath = join15(cwd, SNAPSHOT_DB_REL);
21677
+ const dbPath = join16(cwd, SNAPSHOT_DB_REL);
21510
21678
  try {
21511
21679
  statSync5(dbPath);
21512
21680
  } catch {
@@ -21527,8 +21695,8 @@ async function trySnapshotDbOpen(cwd) {
21527
21695
  }
21528
21696
  }
21529
21697
  function checkSnapshotStore(cwd) {
21530
- const dbPath = join15(cwd, SNAPSHOT_DB_REL);
21531
- const dir = dirname4(dbPath);
21698
+ const dbPath = join16(cwd, SNAPSHOT_DB_REL);
21699
+ const dir = dirname5(dbPath);
21532
21700
  let dirStatus = "ok";
21533
21701
  let dirDetail = dir;
21534
21702
  let dirHint;
@@ -21601,7 +21769,7 @@ async function checkGit(cwd, signal) {
21601
21769
  if (repoCheck && repoCheck.code === 0 && /true/.test(repoCheck.stdout)) {
21602
21770
  const topRes = await runCached(gitCache, TTL.gitRepo, commandForExternal("git"), ["rev-parse", "--show-toplevel"], TIMEOUT.git, signal, cwd);
21603
21771
  const top = topRes?.stdout.trim() || cwd;
21604
- const insideCwd = relative5(top, cwd) || ".";
21772
+ const insideCwd = relative6(top, cwd) || ".";
21605
21773
  rows.push({
21606
21774
  id: "git.repo",
21607
21775
  title: "Working tree",
@@ -22354,29 +22522,20 @@ function parseDoctorCliArgs(argv) {
22354
22522
  json2 = true;
22355
22523
  continue;
22356
22524
  }
22357
- if (arg === "--cwd") {
22358
- const next = argv[++i];
22359
- if (!next)
22360
- return { kind: "error", message: "--cwd requires a value" };
22361
- cwd = next;
22525
+ const global = takeGlobalCliOption(argv, i, {
22526
+ allowedCommands: ["git", "docker", "gh"]
22527
+ });
22528
+ if (global.kind === "error") {
22529
+ return { kind: "error", message: global.error };
22530
+ }
22531
+ if (global.kind === "cwd") {
22532
+ cwd = global.value;
22533
+ i = global.next;
22362
22534
  continue;
22363
22535
  }
22364
- if (arg === "--bin") {
22365
- const next = argv[++i];
22366
- if (!next) {
22367
- return {
22368
- kind: "error",
22369
- message: "--bin requires <name>=<absolute-path>"
22370
- };
22371
- }
22372
- const parsed = parseExternalCommandOverride(next, "--bin", [
22373
- "git",
22374
- "docker",
22375
- "gh"
22376
- ]);
22377
- if (parsed.ok === false)
22378
- return { kind: "error", message: parsed.error };
22379
- commandOverrides.push(parsed.override);
22536
+ if (global.kind === "command-override") {
22537
+ commandOverrides.push(global.override);
22538
+ i = global.next;
22380
22539
  continue;
22381
22540
  }
22382
22541
  if (arg === "--port") {
@@ -22503,6 +22662,7 @@ Exit codes:
22503
22662
  `, STATUS_SYMBOL;
22504
22663
  var init_doctor_cli = __esm(() => {
22505
22664
  init_command_resolver();
22665
+ init_cli_helpers();
22506
22666
  init_doctor();
22507
22667
  init_git();
22508
22668
  STATUS_SYMBOL = {
@@ -22529,44 +22689,6 @@ function normalizeNewDirectoryName(name) {
22529
22689
  return trimmed;
22530
22690
  }
22531
22691
 
22532
- // web-src/server/cache.ts
22533
- import { lstatSync as lstatSync3 } from "node:fs";
22534
- import { join as join16 } from "node:path";
22535
- function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
22536
- return !!cached && now - cached.storedAt <= ttlMs;
22537
- }
22538
- function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
22539
- cache.set(key, { ...value, storedAt: now });
22540
- while (cache.size > maxEntries) {
22541
- const oldest = cache.keys().next().value;
22542
- if (oldest === undefined)
22543
- break;
22544
- cache.delete(oldest);
22545
- }
22546
- }
22547
- function worktreeFileSignature(path, cwd) {
22548
- try {
22549
- const stats = lstatSync3(join16(cwd, path));
22550
- const inode = "ino" in stats ? stats.ino : 0;
22551
- return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
22552
- } catch {
22553
- return "state:missing";
22554
- }
22555
- }
22556
- function fileDiffCacheKey(options) {
22557
- const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
22558
- if (options.isUntracked && !worktreeTarget) {
22559
- throw new Error("untracked file diffs require a worktree range");
22560
- }
22561
- const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
22562
- if (options.isUntracked) {
22563
- return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
22564
- }
22565
- return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
22566
- }
22567
- var CACHE_TTL_MS = 1500, MAX_TIMED_CACHE_ENTRIES = 200;
22568
- var init_cache = () => {};
22569
-
22570
22692
  // web-src/server/dev-assets.ts
22571
22693
  import { basename as basename2 } from "node:path";
22572
22694
  function startDevAssetReload(options) {
@@ -22606,9 +22728,7 @@ function emptyJournalTaskState() {
22606
22728
  return { version: 1, tasks: [] };
22607
22729
  }
22608
22730
  function makeJournalId(prefix) {
22609
- const random = Math.random().toString(36).slice(2, 8);
22610
- const time = Date.now().toString(36);
22611
- return `${prefix}-${time}${random}`;
22731
+ return makeTimedId(prefix);
22612
22732
  }
22613
22733
  function optionalString4(value, maxLen) {
22614
22734
  if (typeof value !== "string")
@@ -22762,11 +22882,8 @@ async function updateDailyJournalState(root, updater) {
22762
22882
  async function updateJournalTaskState(root, updater) {
22763
22883
  return journalTaskStore.update(root, updater);
22764
22884
  }
22765
- function insertOptionCount2(input) {
22766
- return (input.before_id ? 1 : 0) + (input.after_id ? 1 : 0) + (input.position !== undefined ? 1 : 0);
22767
- }
22768
22885
  function taskInsertIndex(tasks, status, input) {
22769
- if (insertOptionCount2(input) > 1)
22886
+ if (orderedInsertOptionCount(input) > 1)
22770
22887
  return { ok: false, error: "use only one of before, after, or position" };
22771
22888
  if (input.before_id || input.after_id) {
22772
22889
  const anchorId = input.before_id || input.after_id || "";
@@ -22934,6 +23051,16 @@ function addJournalTask(state, input, now, makeId3 = makeJournalId) {
22934
23051
  tasks.splice(insertAt.index, 0, task);
22935
23052
  return { ok: true, state: { version: 1, tasks }, task };
22936
23053
  }
23054
+ function journalTaskResult(state, task) {
23055
+ return {
23056
+ ok: true,
23057
+ state: {
23058
+ version: 1,
23059
+ tasks: state.tasks.map((item) => item.id === task.id ? task : item)
23060
+ },
23061
+ task
23062
+ };
23063
+ }
22937
23064
  function updateJournalTask(state, id, patch, now) {
22938
23065
  const task = state.tasks.find((item) => item.id === id);
22939
23066
  if (!task)
@@ -22988,14 +23115,7 @@ function updateJournalTask(state, id, patch, now) {
22988
23115
  else
22989
23116
  delete next.journal_entry_id;
22990
23117
  }
22991
- return {
22992
- ok: true,
22993
- state: {
22994
- version: 1,
22995
- tasks: state.tasks.map((item) => item.id === id ? next : item)
22996
- },
22997
- task: next
22998
- };
23118
+ return journalTaskResult(state, next);
22999
23119
  }
23000
23120
  function moveJournalTask(state, id, input, now) {
23001
23121
  const source = state.tasks.find((task) => task.id === id);
@@ -23105,8 +23225,7 @@ function claimJournalTask(state, id, input, now) {
23105
23225
  if (!task)
23106
23226
  return { ok: false, error: "task not found" };
23107
23227
  const nowMs = Date.parse(now);
23108
- const activeClaim = task.claim && Number.isFinite(Date.parse(task.claim.lease_expires_at)) && Date.parse(task.claim.lease_expires_at) > nowMs;
23109
- if (activeClaim)
23228
+ if (taskClaimActive(task, nowMs))
23110
23229
  return { ok: false, error: "task is already claimed" };
23111
23230
  if (task.status !== "todo" && task.status !== "doing")
23112
23231
  return {
@@ -23117,12 +23236,9 @@ function claimJournalTask(state, id, input, now) {
23117
23236
  const wipLimit = input.wip_limit;
23118
23237
  if (wipLimit !== undefined && wipLimit > 0) {
23119
23238
  const activeDoing = state.tasks.filter((item) => {
23120
- if (item.status !== "doing" || !item.claim)
23239
+ if (item.status !== "doing" || item.claim?.by !== by)
23121
23240
  return false;
23122
- if (item.claim.by !== by)
23123
- return false;
23124
- const expires = Date.parse(item.claim.lease_expires_at);
23125
- return Number.isFinite(expires) && expires > nowMs;
23241
+ return taskClaimActive(item, nowMs);
23126
23242
  }).length;
23127
23243
  if (activeDoing >= wipLimit)
23128
23244
  return { ok: false, error: "WIP limit reached" };
@@ -23139,14 +23255,7 @@ function claimJournalTask(state, id, input, now) {
23139
23255
  lease_expires_at: leaseExpiresAt
23140
23256
  }
23141
23257
  };
23142
- return {
23143
- ok: true,
23144
- state: {
23145
- version: 1,
23146
- tasks: state.tasks.map((item) => item.id === id ? next : item)
23147
- },
23148
- task: next
23149
- };
23258
+ return journalTaskResult(state, next);
23150
23259
  }
23151
23260
  function completeJournalTask(state, id, input, now, makeId3 = makeJournalId) {
23152
23261
  const task = state.tasks.find((item) => item.id === id);
@@ -23155,8 +23264,7 @@ function completeJournalTask(state, id, input, now, makeId3 = makeJournalId) {
23155
23264
  if (task.status !== "doing")
23156
23265
  return { ok: false, error: "only doing tasks can be completed" };
23157
23266
  const nowMs = Date.parse(now);
23158
- const activeClaim = task.claim && Number.isFinite(Date.parse(task.claim.lease_expires_at)) && Date.parse(task.claim.lease_expires_at) > nowMs;
23159
- if (!activeClaim)
23267
+ if (!taskClaimActive(task, nowMs))
23160
23268
  return { ok: false, error: "task must be claimed before completion" };
23161
23269
  const by = optionalString4(input.by, 128);
23162
23270
  if (!by)
@@ -23186,14 +23294,7 @@ function completeJournalTask(state, id, input, now, makeId3 = makeJournalId) {
23186
23294
  ...notes.length ? { notes } : {}
23187
23295
  };
23188
23296
  delete next.claim;
23189
- return {
23190
- ok: true,
23191
- state: {
23192
- version: 1,
23193
- tasks: state.tasks.map((item) => item.id === id ? next : item)
23194
- },
23195
- task: next
23196
- };
23297
+ return journalTaskResult(state, next);
23197
23298
  }
23198
23299
  function deleteJournalTask(state, id) {
23199
23300
  const tasks = state.tasks.filter((task) => task.id !== id);
@@ -23229,8 +23330,8 @@ var init_journal2 = __esm(() => {
23229
23330
  });
23230
23331
 
23231
23332
  // web-src/server/search-service.ts
23232
- import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "node:fs";
23233
- import { join as join18, relative as relative6 } from "node:path";
23333
+ import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync7, realpathSync as realpathSync6 } from "node:fs";
23334
+ import { join as join18, relative as relative7 } from "node:path";
23234
23335
  async function rgAvailableAsync(cwd) {
23235
23336
  if (rgAvailableCache !== null)
23236
23337
  return rgAvailableCache;
@@ -23266,12 +23367,12 @@ function safeWorktreePath(env, path) {
23266
23367
  let realCwd;
23267
23368
  let realFull;
23268
23369
  try {
23269
- realCwd = realpathSync5(env.cwd);
23270
- realFull = realpathSync5(full);
23370
+ realCwd = realpathSync6(env.cwd);
23371
+ realFull = realpathSync6(full);
23271
23372
  } catch {
23272
23373
  return null;
23273
23374
  }
23274
- const rel = relative6(realCwd, realFull);
23375
+ const rel = relative7(realCwd, realFull);
23275
23376
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
23276
23377
  return null;
23277
23378
  if (isGitInternalPath(rel))
@@ -25015,7 +25116,7 @@ import {
25015
25116
  mkdirSync as mkdirSync4,
25016
25117
  openSync as openSync3,
25017
25118
  readFileSync as readFileSync9,
25018
- realpathSync as realpathSync6,
25119
+ realpathSync as realpathSync7,
25019
25120
  renameSync,
25020
25121
  statSync as statSync6,
25021
25122
  unlinkSync as unlinkSync2,
@@ -25023,7 +25124,7 @@ import {
25023
25124
  writeFileSync as writeFileSync2
25024
25125
  } from "node:fs";
25025
25126
  import { homedir as homedir3 } from "node:os";
25026
- import { basename as basename3, dirname as dirname5, extname as extname2, join as join20, relative as relative7 } from "node:path";
25127
+ import { basename as basename3, dirname as dirname6, extname as extname2, join as join20, relative as relative8 } from "node:path";
25027
25128
  function parseCli() {
25028
25129
  const rest = [];
25029
25130
  for (let i = 2;i < process.argv.length; i++) {
@@ -25077,7 +25178,7 @@ Examples:
25077
25178
  process.exit(1);
25078
25179
  }
25079
25180
  try {
25080
- cwd = realpathSync6(next);
25181
+ cwd = realpathSync7(next);
25081
25182
  cwdWasExplicit = true;
25082
25183
  } catch {
25083
25184
  console.error("--cwd must point to an existing directory");
@@ -25508,7 +25609,7 @@ function worktreePath(path) {
25508
25609
  function safeOpenWorktreePath(path) {
25509
25610
  if (path === "") {
25510
25611
  try {
25511
- const realCwd = realpathSync6(cwd);
25612
+ const realCwd = realpathSync7(cwd);
25512
25613
  if (isGitInternalPath(realCwd))
25513
25614
  return null;
25514
25615
  return realCwd;
@@ -25519,7 +25620,7 @@ function safeOpenWorktreePath(path) {
25519
25620
  return safeWorktreePath2(path);
25520
25621
  }
25521
25622
  function parentRepoPath(path) {
25522
- const parent = dirname5(path);
25623
+ const parent = dirname6(path);
25523
25624
  return parent === "." ? "" : parent;
25524
25625
  }
25525
25626
  function isoDate(ms) {
@@ -25577,6 +25678,21 @@ async function attachTreeEntryMetadata(target, entry) {
25577
25678
  return { ...entry, ...await directoryMetadata(target, entry.path) };
25578
25679
  if (entry.type !== "blob")
25579
25680
  return entry;
25681
+ if (entry.is_symlink && target !== "worktree" && target !== "") {
25682
+ const symlinkMeta = await gitSymlinkTargetMetadataAsync(target, entry.path, cwd);
25683
+ if (symlinkMeta.symlink_target_type === "tree")
25684
+ return {
25685
+ ...entry,
25686
+ ...symlinkMeta,
25687
+ type: "tree",
25688
+ ...await directoryMetadata(target, entry.path)
25689
+ };
25690
+ return {
25691
+ ...entry,
25692
+ ...symlinkMeta,
25693
+ ...await fileMetadataForTarget(target, entry.path)
25694
+ };
25695
+ }
25580
25696
  return { ...entry, ...await fileMetadataForTarget(target, entry.path) };
25581
25697
  }
25582
25698
  async function readReadme(target, dirPath) {
@@ -25599,6 +25715,22 @@ async function readReadme(target, dirPath) {
25599
25715
  }
25600
25716
  return null;
25601
25717
  }
25718
+ function deletedTreeEntriesForPath(statusMap, basePath) {
25719
+ const entries = [];
25720
+ for (const [path, status] of statusMap) {
25721
+ if (status !== "D")
25722
+ continue;
25723
+ if (basePath) {
25724
+ if (!path.startsWith(`${basePath}/`))
25725
+ continue;
25726
+ }
25727
+ const rel = basePath ? path.slice(basePath.length + 1) : path;
25728
+ if (!rel || rel.includes("/"))
25729
+ continue;
25730
+ entries.push({ name: rel, path, type: "blob", status: "D" });
25731
+ }
25732
+ return entries;
25733
+ }
25602
25734
  async function handleTree(url) {
25603
25735
  const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
25604
25736
  const path = (url.searchParams.get("path") || "").replace(/^\/+|\/+$/g, "");
@@ -25625,12 +25757,21 @@ async function handleTree(url) {
25625
25757
  if (tree.error)
25626
25758
  return text(tree.error, tree.status ?? 500);
25627
25759
  const entries = tree.entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
25760
+ const statusMap = target === "worktree" || target === "" ? await repoStatusMapAsync(cwd) : null;
25761
+ const withStatus = (entry) => {
25762
+ const status = statusMap?.get(entry.path);
25763
+ return status ? { ...entry, status } : entry;
25764
+ };
25765
+ const deletedEntries = !recursive && statusMap ? deletedTreeEntriesForPath(statusMap, path) : [];
25628
25766
  return json2({
25629
25767
  ref: target,
25630
25768
  path,
25631
25769
  project: basename3(cwd),
25632
25770
  branch: await currentBranchMetadata(),
25633
- entries: recursive ? entries : await Promise.all(entries.map((entry) => attachTreeEntryMetadata(target, entry))),
25771
+ entries: recursive ? entries.map(withStatus) : [
25772
+ ...await Promise.all(entries.map((entry) => attachTreeEntryMetadata(target, entry).then(withStatus))),
25773
+ ...deletedEntries
25774
+ ],
25634
25775
  readme: await readReadme(target, path),
25635
25776
  upload_enabled: uploadEnabled && (target === "worktree" || target === "")
25636
25777
  });
@@ -26326,7 +26467,7 @@ async function handleUploadFiles(req) {
26326
26467
  if (total > MAX_UPLOAD_TOTAL_BYTES)
26327
26468
  return text("upload too large", 413);
26328
26469
  const target = join20(realDir, safeName);
26329
- if (relative7(realDir, dirname5(target)) !== "")
26470
+ if (relative8(realDir, dirname6(target)) !== "")
26330
26471
  return text("invalid filename", 400);
26331
26472
  if (existsSync8(target))
26332
26473
  return text("file exists", 409);
@@ -26492,10 +26633,10 @@ async function restoreTrashPath(originalPath, trashPath) {
26492
26633
  return { ok: false, error: "trash item not found" };
26493
26634
  try {
26494
26635
  const trashRoot = join20(homedir3(), ".Trash");
26495
- const trashRelative = relative7(trashRoot, trashPath);
26636
+ const trashRelative = relative8(trashRoot, trashPath);
26496
26637
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
26497
26638
  return { ok: false, error: "invalid trash handle" };
26498
- mkdirSync4(dirname5(original), { recursive: true });
26639
+ mkdirSync4(dirname6(original), { recursive: true });
26499
26640
  renameSync(trashPath, original);
26500
26641
  return { ok: true };
26501
26642
  } catch (error) {