@youtyan/code-viewer 0.12.0 → 0.13.1

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.
@@ -213,6 +213,14 @@ function buildRoute(route) {
213
213
  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" : "");
214
214
  case "diff":
215
215
  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))}` : "");
216
+ case "worktree": {
217
+ const params = new URLSearchParams();
218
+ if (route.wt) params.set("wt", route.wt);
219
+ if (route.file) params.set("file", route.file);
220
+ if (route.file && route.origin) params.set("origin", route.origin);
221
+ const qs = params.toString();
222
+ return `/worktree${qs ? `?${qs}` : ""}`;
223
+ }
216
224
  case "help": {
217
225
  const params = new URLSearchParams();
218
226
  if (route.lang && route.lang !== "en") params.set("lang", route.lang);
@@ -267,6 +275,7 @@ var init_routes = __esm({
267
275
  "/history",
268
276
  "/journal",
269
277
  "/database",
278
+ "/worktree",
270
279
  "/doctor"
271
280
  ];
272
281
  APP_ENTRY_PATHS = ["/", "/index.html"];
@@ -1197,6 +1206,127 @@ var init_command_resolver = __esm({
1197
1206
  }
1198
1207
  });
1199
1208
 
1209
+ // web-src/core/worktree.ts
1210
+ function worktreeNameError(name) {
1211
+ if (!name) return "empty";
1212
+ if (name.length > WORKTREE_NAME_MAX_LENGTH) return "too-long";
1213
+ for (const ch of name) {
1214
+ const code = ch.charCodeAt(0);
1215
+ if (code < 32 || code === 127) return "control";
1216
+ }
1217
+ if (name.includes("/") || name.includes("\\")) return "separator";
1218
+ if (name === "." || name === "..") return "relative";
1219
+ if (name.startsWith(".")) return "leading-dot";
1220
+ return null;
1221
+ }
1222
+ function worktreeBranchError(branch) {
1223
+ if (!branch) return "empty";
1224
+ if (branch.length > WORKTREE_NAME_MAX_LENGTH) return "too-long";
1225
+ for (const ch of branch) {
1226
+ const code = ch.charCodeAt(0);
1227
+ if (code < 32 || code === 127) return "control";
1228
+ }
1229
+ if (branch.startsWith("-") || branch.includes("..")) return "relative";
1230
+ return null;
1231
+ }
1232
+ function emptyRef(path) {
1233
+ return {
1234
+ path,
1235
+ head: "",
1236
+ branch: "",
1237
+ detached: false,
1238
+ bare: false,
1239
+ locked: false,
1240
+ lockedReason: "",
1241
+ prunable: false,
1242
+ prunableReason: ""
1243
+ };
1244
+ }
1245
+ function parseWorktreeList(stdout) {
1246
+ const refs = [];
1247
+ let current = null;
1248
+ const nulDelimited = stdout.includes("\0");
1249
+ const records = nulDelimited ? stdout.split("\0") : stdout.split("\n");
1250
+ for (const rawRecord of records) {
1251
+ const line = nulDelimited ? rawRecord : rawRecord.replace(/\r$/, "");
1252
+ if (!line) continue;
1253
+ if (line.startsWith("worktree ")) {
1254
+ current = emptyRef(line.slice("worktree ".length));
1255
+ refs.push(current);
1256
+ continue;
1257
+ }
1258
+ if (!current) continue;
1259
+ if (line.startsWith("HEAD ")) {
1260
+ current.head = line.slice("HEAD ".length);
1261
+ } else if (line.startsWith("branch ")) {
1262
+ const ref = line.slice("branch ".length);
1263
+ current.branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
1264
+ } else if (line === "detached") {
1265
+ current.detached = true;
1266
+ } else if (line === "bare") {
1267
+ current.bare = true;
1268
+ } else if (line === "locked" || line.startsWith("locked ")) {
1269
+ current.locked = true;
1270
+ current.lockedReason = line.slice("locked".length).trim();
1271
+ } else if (line === "prunable" || line.startsWith("prunable ")) {
1272
+ current.prunable = true;
1273
+ current.prunableReason = line.slice("prunable".length).trim();
1274
+ }
1275
+ }
1276
+ return refs;
1277
+ }
1278
+ function findWorktree(worktrees, path) {
1279
+ return worktrees.find((entry) => entry.path === path) || null;
1280
+ }
1281
+ function parseAheadBehind(stdout) {
1282
+ const parts = stdout.trim().split(/\s+/);
1283
+ if (parts.length < 2) return null;
1284
+ const behind = Number(parts[0]);
1285
+ const ahead = Number(parts[1]);
1286
+ if (!Number.isInteger(behind) || !Number.isInteger(ahead)) return null;
1287
+ if (behind < 0 || ahead < 0) return null;
1288
+ return { behind, ahead };
1289
+ }
1290
+ function parseMergeTreeConflicts(stdout) {
1291
+ const lines = stdout.split("\n");
1292
+ const conflicts = [];
1293
+ for (let i = 1; i < lines.length; i++) {
1294
+ const line = lines[i].replace(/\r$/, "");
1295
+ if (!line) break;
1296
+ conflicts.push(line);
1297
+ }
1298
+ return conflicts;
1299
+ }
1300
+ function findWorktreeOverlaps(items) {
1301
+ const byPath = /* @__PURE__ */ new Map();
1302
+ for (const item of items) {
1303
+ const counted = /* @__PURE__ */ new Set();
1304
+ for (const file of item.files) {
1305
+ for (const path of [file.path, file.oldPath]) {
1306
+ if (!path || counted.has(path)) continue;
1307
+ counted.add(path);
1308
+ const owners = byPath.get(path);
1309
+ if (owners) owners.push(item.id);
1310
+ else byPath.set(path, [item.id]);
1311
+ }
1312
+ }
1313
+ }
1314
+ const overlaps = [];
1315
+ for (const [path, worktreeIds] of byPath) {
1316
+ if (worktreeIds.length >= 2) overlaps.push({ path, worktreeIds });
1317
+ }
1318
+ overlaps.sort(
1319
+ (a, b) => b.worktreeIds.length - a.worktreeIds.length || (a.path < b.path ? -1 : 1)
1320
+ );
1321
+ return overlaps;
1322
+ }
1323
+ var WORKTREE_NAME_MAX_LENGTH;
1324
+ var init_worktree = __esm({
1325
+ "web-src/core/worktree.ts"() {
1326
+ WORKTREE_NAME_MAX_LENGTH = 100;
1327
+ }
1328
+ });
1329
+
1200
1330
  // web-src/server/cache.ts
1201
1331
  import { lstatSync } from "node:fs";
1202
1332
  import { join as join3 } from "node:path";
@@ -1389,7 +1519,8 @@ function runBytesAsync(args, cwd2, options = {}) {
1389
1519
  return new Promise((resolve4) => {
1390
1520
  const proc = spawn(args[0], args.slice(1), {
1391
1521
  cwd: cwd2,
1392
- stdio: [options.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
1522
+ stdio: [options.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
1523
+ signal: options.signal
1393
1524
  });
1394
1525
  if (options.stdin !== void 0) {
1395
1526
  proc.stdin?.on("error", () => {
@@ -1686,8 +1817,8 @@ function run(args, cwd2) {
1686
1817
  }
1687
1818
  function runGitAsync(args, cwd2, options = {}) {
1688
1819
  return runAsync(resolveGitArgs(args), cwd2, {
1689
- timeout: GIT_COMMAND_TIMEOUT_MS,
1690
- ...options
1820
+ ...options,
1821
+ timeout: options.timeout ?? GIT_COMMAND_TIMEOUT_MS
1691
1822
  });
1692
1823
  }
1693
1824
  function resolveGitArgs(args) {
@@ -1714,17 +1845,140 @@ async function runGitRefLookupAsync(args, cwd2) {
1714
1845
  function repoRoot(cwd2) {
1715
1846
  return runGitRefLookup(["git", "rev-parse", "--show-toplevel"], cwd2);
1716
1847
  }
1848
+ async function worktreeListResultAsync(cwd2, options = {}) {
1849
+ const res = await runGitAsync(
1850
+ ["git", "worktree", "list", "--porcelain", "-z"],
1851
+ cwd2,
1852
+ options
1853
+ );
1854
+ if (res.code !== 0) {
1855
+ return {
1856
+ worktrees: [],
1857
+ ...gitFailureResult(res, "git worktree list failed")
1858
+ };
1859
+ }
1860
+ return { worktrees: parseWorktreeList(res.stdout) };
1861
+ }
1717
1862
  async function worktreePathsAsync(cwd2) {
1863
+ const res = await worktreeListResultAsync(cwd2);
1864
+ return res.worktrees.map((entry) => entry.path);
1865
+ }
1866
+ async function localBranchExistsResultAsync(cwd2, branch) {
1867
+ const result = await runGitAsync(
1868
+ ["git", "rev-parse", "--verify", "--quiet", `refs/heads/${branch}`],
1869
+ cwd2
1870
+ );
1871
+ if (result.code === 0) return { exists: true };
1872
+ if (result.code === 1) return { exists: false };
1873
+ return {
1874
+ exists: false,
1875
+ ...gitFailureResult(result, "git branch lookup failed")
1876
+ };
1877
+ }
1878
+ async function defaultBranchResultAsync(cwd2) {
1879
+ const headResult = await runGitAsync(
1880
+ ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
1881
+ cwd2
1882
+ );
1883
+ if (headResult.code !== 0 && headResult.code !== 1) {
1884
+ return {
1885
+ branch: "",
1886
+ ...gitFailureResult(headResult, "git default branch lookup failed")
1887
+ };
1888
+ }
1889
+ const head = headResult.code === 0 ? headResult.stdout.trimEnd() : "";
1890
+ const fromRemote = head?.startsWith("origin/") ? head.slice("origin/".length) : head;
1891
+ if (fromRemote) {
1892
+ const exists = await localBranchExistsResultAsync(cwd2, fromRemote);
1893
+ if (exists.error) {
1894
+ return { branch: "", error: exists.error, status: exists.status };
1895
+ }
1896
+ if (exists.exists) return { branch: fromRemote };
1897
+ }
1898
+ for (const name of ["main", "master"]) {
1899
+ const exists = await localBranchExistsResultAsync(cwd2, name);
1900
+ if (exists.error) {
1901
+ return { branch: "", error: exists.error, status: exists.status };
1902
+ }
1903
+ if (exists.exists) return { branch: name };
1904
+ }
1905
+ return { branch: "" };
1906
+ }
1907
+ async function worktreeDivergenceResultAsync(cwd2, base, ref) {
1718
1908
  const res = await runGitAsync(
1719
- ["git", "worktree", "list", "--porcelain"],
1909
+ [
1910
+ "git",
1911
+ "rev-list",
1912
+ "--left-right",
1913
+ "--count",
1914
+ `refs/heads/${base}...refs/heads/${ref}`
1915
+ ],
1720
1916
  cwd2
1721
1917
  );
1722
- if (res.code !== 0) return [];
1723
- const paths = [];
1724
- for (const line of res.stdout.split("\n")) {
1725
- if (line.startsWith("worktree ")) paths.push(line.slice(9).trimEnd());
1918
+ if (res.code !== 0) {
1919
+ return {
1920
+ behind: 0,
1921
+ ahead: 0,
1922
+ ...gitFailureResult(res, "git rev-list failed")
1923
+ };
1924
+ }
1925
+ const parsed = parseAheadBehind(res.stdout);
1926
+ if (!parsed) {
1927
+ return {
1928
+ behind: 0,
1929
+ ahead: 0,
1930
+ error: `unexpected rev-list output: ${res.stdout.trim()}`
1931
+ };
1932
+ }
1933
+ return parsed;
1934
+ }
1935
+ async function mergePreviewResultAsync(cwd2, base, ref) {
1936
+ const res = await runGitAsync(
1937
+ [
1938
+ "git",
1939
+ "merge-tree",
1940
+ "--write-tree",
1941
+ "--name-only",
1942
+ `refs/heads/${base}`,
1943
+ `refs/heads/${ref}`
1944
+ ],
1945
+ cwd2
1946
+ );
1947
+ if (res.code === 0) return { state: "clean", conflicts: [] };
1948
+ if (res.code === 1) {
1949
+ return {
1950
+ state: "conflict",
1951
+ conflicts: parseMergeTreeConflicts(res.stdout)
1952
+ };
1726
1953
  }
1727
- return paths;
1954
+ return {
1955
+ state: "unknown",
1956
+ conflicts: [],
1957
+ error: gitFailureMessage(res, "git merge-tree failed")
1958
+ };
1959
+ }
1960
+ async function worktreeAddResultAsync(cwd2, options) {
1961
+ const args = options.createBranch ? ["git", "worktree", "add", "-b", options.branch, options.path] : ["git", "worktree", "add", options.path, options.branch];
1962
+ const res = await runGitAsync(args, cwd2);
1963
+ if (res.code !== 0) return gitFailureResult(res, "git worktree add failed");
1964
+ return {};
1965
+ }
1966
+ async function worktreeRemoveResultAsync(cwd2, options) {
1967
+ const args = ["git", "worktree", "remove"];
1968
+ if (options.force) args.push("--force");
1969
+ args.push("--", options.path);
1970
+ const res = await runGitAsync(args, cwd2);
1971
+ if (res.code !== 0) {
1972
+ return gitFailureResult(res, "git worktree remove failed");
1973
+ }
1974
+ return {};
1975
+ }
1976
+ async function worktreePruneResultAsync(cwd2) {
1977
+ const res = await runGitAsync(["git", "worktree", "prune"], cwd2);
1978
+ if (res.code !== 0) {
1979
+ return gitFailureResult(res, "git worktree prune failed");
1980
+ }
1981
+ return {};
1728
1982
  }
1729
1983
  function repoRootResult(cwd2) {
1730
1984
  const res = run(["git", "rev-parse", "--show-toplevel"], cwd2);
@@ -1771,28 +2025,9 @@ async function statusPorcelainForPathAsync(path, cwd2) {
1771
2025
  error: gitFailureMessage(res, "git status failed")
1772
2026
  };
1773
2027
  }
1774
- async function repoStatusMapAsync(cwd2, now = Date.now()) {
1775
- const cached = repoStatusMapCache.get(cwd2);
1776
- if (cacheFresh(cached, now)) return cached.map;
2028
+ function parseStatusPorcelainZ(stdout) {
1777
2029
  const map = /* @__PURE__ */ new Map();
1778
- const res = await runGitAsync(
1779
- [
1780
- "git",
1781
- "-c",
1782
- "core.quotepath=false",
1783
- "status",
1784
- "--porcelain=v1",
1785
- "-z",
1786
- // "normal" (not "all") keeps a brand-new untracked directory collapsed
1787
- // to a single `?? dir/` record instead of walking into it: the tree
1788
- // explorer badges its descendants by ancestor lookup anyway, and "all"
1789
- // would make git enumerate every file under it on each poll.
1790
- "--untracked-files=normal"
1791
- ],
1792
- cwd2
1793
- );
1794
- if (res.code !== 0) return map;
1795
- const records = res.stdout.split("\0").filter(Boolean);
2030
+ const records = stdout.split("\0").filter(Boolean);
1796
2031
  for (let i = 0; i < records.length; i++) {
1797
2032
  const record = records[i];
1798
2033
  const xy = record.slice(0, 2);
@@ -1810,6 +2045,14 @@ async function repoStatusMapAsync(cwd2, now = Date.now()) {
1810
2045
  const code = xy[0] !== " " ? xy[0] : xy[1];
1811
2046
  if (code && code !== " ") map.set(path, code);
1812
2047
  }
2048
+ return map;
2049
+ }
2050
+ async function repoStatusMapAsync(cwd2, now = Date.now()) {
2051
+ const cached = repoStatusMapCache.get(cwd2);
2052
+ if (cacheFresh(cached, now)) return cached.map;
2053
+ const res = await runGitAsync(STATUS_PORCELAIN_ARGS, cwd2);
2054
+ if (res.code !== 0) return /* @__PURE__ */ new Map();
2055
+ const map = parseStatusPorcelainZ(res.stdout);
1813
2056
  setTimedCacheEntry(repoStatusMapCache, cwd2, { map }, now);
1814
2057
  return map;
1815
2058
  }
@@ -2372,8 +2615,8 @@ function isGitInternalPath(path) {
2372
2615
  function syntheticUncommittedBlameFromWorktree(cwd2, path) {
2373
2616
  const filePath = join4(cwd2, path);
2374
2617
  try {
2375
- const stat3 = statSync3(filePath);
2376
- if (!stat3.isFile()) return { lines: [], commits: {}, error: "not a file" };
2618
+ const stat4 = statSync3(filePath);
2619
+ if (!stat4.isFile()) return { lines: [], commits: {}, error: "not a file" };
2377
2620
  const text2 = readFileSync(filePath, "utf8");
2378
2621
  const normalized = text2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2379
2622
  const lineCount = normalized.length ? normalized.endsWith("\n") ? normalized.length - 1 === 0 ? 1 : normalized.split("\n").length - 1 : normalized.split("\n").length : 1;
@@ -2493,11 +2736,18 @@ async function blameAsync(cwd2, options) {
2493
2736
  return { lines, commits };
2494
2737
  }
2495
2738
  async function untrackedAsync(cwd2, path = "") {
2496
- const args = ["git", "ls-files", "--others", "--exclude-standard"];
2739
+ const args = ["git", "ls-files", "-z", "--others", "--exclude-standard"];
2497
2740
  if (path) args.push("--", `${path}/`);
2498
2741
  const res = await runGitAsync(args, cwd2);
2499
- if (res.code !== 0) return [];
2500
- return res.stdout.split("\n").filter(Boolean).filter((entry) => !isToolInternalPath(entry));
2742
+ if (res.code !== 0) {
2743
+ return {
2744
+ paths: [],
2745
+ ...gitFailureResult(res, "git untracked file lookup failed")
2746
+ };
2747
+ }
2748
+ return {
2749
+ paths: res.stdout.split("\0").filter(Boolean).filter((entry) => !isToolInternalPath(entry))
2750
+ };
2501
2751
  }
2502
2752
  function normalizeTreePath(path) {
2503
2753
  return path.replace(/^\/+|\/+$/g, "");
@@ -2549,8 +2799,8 @@ function resolveWorktreeSymlinkTarget(cwd2, full) {
2549
2799
  let symlink_target_type = "missing";
2550
2800
  if (realpathWithinRepo(cwd2, full, false) !== null) {
2551
2801
  try {
2552
- const stat3 = statSync3(full);
2553
- symlink_target_type = stat3.isDirectory() ? "tree" : stat3.isFile() ? "blob" : "missing";
2802
+ const stat4 = statSync3(full);
2803
+ symlink_target_type = stat4.isDirectory() ? "tree" : stat4.isFile() ? "blob" : "missing";
2554
2804
  } catch {
2555
2805
  symlink_target_type = "missing";
2556
2806
  }
@@ -2809,10 +3059,15 @@ async function scanFileBinaryAndNewlinesAsync(full) {
2809
3059
  return { binary: false, newlines };
2810
3060
  }
2811
3061
  async function untrackedMetaAsync(cwd2) {
2812
- const paths = await untrackedAsync(cwd2);
3062
+ const listed = await untrackedAsync(cwd2);
3063
+ if (listed.error) {
3064
+ return { files: [], error: listed.error, status: listed.status };
3065
+ }
3066
+ const paths = listed.paths;
2813
3067
  const previous = untrackedScanCache.get(cwd2);
2814
3068
  const next = /* @__PURE__ */ new Map();
2815
3069
  const results = new Array(paths.length).fill(null);
3070
+ const errors = new Array(paths.length).fill(null);
2816
3071
  let cursor = 0;
2817
3072
  const worker = async () => {
2818
3073
  while (cursor < paths.length) {
@@ -2822,7 +3077,8 @@ async function untrackedMetaAsync(cwd2) {
2822
3077
  let stats;
2823
3078
  try {
2824
3079
  stats = await stat(full);
2825
- } catch {
3080
+ } catch (error) {
3081
+ errors[index] = `failed to stat untracked file ${path}: ${formatErrorDetail(error)}`;
2826
3082
  continue;
2827
3083
  }
2828
3084
  if (!stats.isFile()) continue;
@@ -2834,7 +3090,8 @@ async function untrackedMetaAsync(cwd2) {
2834
3090
  } else {
2835
3091
  try {
2836
3092
  scan = await scanFileBinaryAndNewlinesAsync(full);
2837
- } catch {
3093
+ } catch (error) {
3094
+ errors[index] = `failed to read untracked file ${path}: ${formatErrorDetail(error)}`;
2838
3095
  continue;
2839
3096
  }
2840
3097
  }
@@ -2851,7 +3108,10 @@ async function untrackedMetaAsync(cwd2) {
2851
3108
  )
2852
3109
  );
2853
3110
  untrackedScanCache.set(cwd2, next);
2854
- return results.filter((meta) => meta !== null);
3111
+ return {
3112
+ files: results.filter((meta) => meta !== null),
3113
+ ...errors.some(Boolean) ? { error: errors.filter((error) => !!error).join("\n") } : {}
3114
+ };
2855
3115
  }
2856
3116
  async function fileMetaResultAsync(args, cwd2, includeUntracked2 = false) {
2857
3117
  let ns;
@@ -2877,8 +3137,11 @@ async function fileMetaResultAsync(args, cwd2, includeUntracked2 = false) {
2877
3137
  binary: stats?.binary || false
2878
3138
  };
2879
3139
  });
3140
+ if (!includeUntracked2) return { files };
3141
+ const untracked = await untrackedMetaAsync(cwd2);
2880
3142
  return {
2881
- files: includeUntracked2 ? files.concat(await untrackedMetaAsync(cwd2)) : files
3143
+ files: files.concat(untracked.files),
3144
+ ...untracked.error ? { error: untracked.error, status: untracked.status } : {}
2882
3145
  };
2883
3146
  }
2884
3147
  async function fileDiffTextAsync(args, path, cwd2) {
@@ -2915,7 +3178,12 @@ async function untrackedFileDiffAsync(extras, path, cwd2) {
2915
3178
  "--no-index",
2916
3179
  ...extras,
2917
3180
  "/dev/null",
2918
- path
3181
+ // `--no-index` は `--` によるオプション終端を受け付けない (usage エラー
3182
+ // になる) ので、`./` を前置してオプションに見えなくする。これが無いと
3183
+ // path が `--output=/tmp/x` のときに git がオプションとして解釈し、
3184
+ // **任意の場所へファイルを書けてしまう** (再現確認済み)。
3185
+ // 呼び出し側でのパス検証と合わせた二重の防御。
3186
+ path.startsWith("./") ? path : `./${path}`
2919
3187
  ],
2920
3188
  cwd2
2921
3189
  );
@@ -2990,9 +3258,11 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
2990
3258
  lineTruncated
2991
3259
  };
2992
3260
  }
2993
- var BLAME_ZERO_SHA, WORKTREE_RECURSIVE_DEPTH_LIMIT, WORKTREE_RECURSIVE_ENTRY_LIMIT, DEFAULT_REF_COMMIT_LIMIT, MAX_REF_COMMIT_LIMIT, COMMIT_FORMAT, ALWAYS_WORKTREE_OMIT_DIR_NAMES, DEFAULT_WORKTREE_OMIT_DIR_NAMES, GIT_COMMAND_TIMEOUT_MS, repoStatusMapCache, HISTORY_FORMAT, MAX_HISTORY_LIMIT, LS_TREE_SYMLINK_MODE, untrackedScanCache, UNTRACKED_SCAN_CONCURRENCY;
3261
+ var BLAME_ZERO_SHA, WORKTREE_RECURSIVE_DEPTH_LIMIT, WORKTREE_RECURSIVE_ENTRY_LIMIT, DEFAULT_REF_COMMIT_LIMIT, MAX_REF_COMMIT_LIMIT, COMMIT_FORMAT, ALWAYS_WORKTREE_OMIT_DIR_NAMES, DEFAULT_WORKTREE_OMIT_DIR_NAMES, GIT_COMMAND_TIMEOUT_MS, repoStatusMapCache, STATUS_PORCELAIN_ARGS, HISTORY_FORMAT, MAX_HISTORY_LIMIT, LS_TREE_SYMLINK_MODE, untrackedScanCache, UNTRACKED_SCAN_CONCURRENCY;
2994
3262
  var init_git = __esm({
2995
3263
  "web-src/server/git.ts"() {
3264
+ init_error_detail();
3265
+ init_worktree();
2996
3266
  init_cache();
2997
3267
  init_command_resolver();
2998
3268
  init_name_pattern();
@@ -3050,6 +3320,19 @@ var init_git = __esm({
3050
3320
  ];
3051
3321
  GIT_COMMAND_TIMEOUT_MS = 2e4;
3052
3322
  repoStatusMapCache = /* @__PURE__ */ new Map();
3323
+ STATUS_PORCELAIN_ARGS = [
3324
+ "git",
3325
+ "-c",
3326
+ "core.quotepath=false",
3327
+ "status",
3328
+ "--porcelain=v1",
3329
+ "-z",
3330
+ // "normal" (not "all") keeps a brand-new untracked directory collapsed
3331
+ // to a single `?? dir/` record instead of walking into it: the tree
3332
+ // explorer badges its descendants by ancestor lookup anyway, and "all"
3333
+ // would make git enumerate every file under it on each poll.
3334
+ "--untracked-files=normal"
3335
+ ];
3053
3336
  HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b";
3054
3337
  MAX_HISTORY_LIMIT = 200;
3055
3338
  LS_TREE_SYMLINK_MODE = "120000";
@@ -3059,14 +3342,8 @@ var init_git = __esm({
3059
3342
  });
3060
3343
 
3061
3344
  // web-src/server/server-registry.ts
3062
- import { createHash } from "node:crypto";
3063
- import {
3064
- existsSync as existsSync2,
3065
- mkdirSync,
3066
- readFileSync as readFileSync2,
3067
- unlinkSync,
3068
- writeFileSync
3069
- } from "node:fs";
3345
+ import { createHash, randomUUID } from "node:crypto";
3346
+ import { mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
3070
3347
  import { homedir } from "node:os";
3071
3348
  import { join as join5 } from "node:path";
3072
3349
  function registryDir() {
@@ -3078,46 +3355,142 @@ function serverRegistryFilePath(root) {
3078
3355
  const hash = createHash("sha256").update(root).digest("hex").slice(0, 16);
3079
3356
  return join5(registryDir(), `${hash}.json`);
3080
3357
  }
3081
- function writeServerRegistry(entry) {
3358
+ function serverStartLockFilePath(root) {
3359
+ const hash = createHash("sha256").update(root).digest("hex").slice(0, 16);
3360
+ return join5(registryDir(), `${hash}.start.lock`);
3361
+ }
3362
+ function errno(error) {
3363
+ return error.code;
3364
+ }
3365
+ function processAlive(pid) {
3082
3366
  try {
3083
- mkdirSync(registryDir(), { recursive: true });
3084
- writeFileSync(
3085
- serverRegistryFilePath(entry.root),
3086
- `${JSON.stringify(entry, null, 2)}
3087
- `,
3088
- "utf8"
3367
+ process.kill(pid, 0);
3368
+ return true;
3369
+ } catch (error) {
3370
+ if (errno(error) === "ESRCH") return false;
3371
+ if (errno(error) === "EPERM") return true;
3372
+ throw error;
3373
+ }
3374
+ }
3375
+ function readServerStartLock(root) {
3376
+ const file = serverStartLockFilePath(root);
3377
+ let raw;
3378
+ try {
3379
+ raw = JSON.parse(readFileSync2(file, "utf8"));
3380
+ } catch (error) {
3381
+ if (errno(error) === "ENOENT") return null;
3382
+ throw errorWithCause(`failed to read server start lock for ${root}`, error);
3383
+ }
3384
+ if (!raw || typeof raw !== "object") {
3385
+ throw new Error(
3386
+ `invalid server start lock for ${root}: expected an object`
3089
3387
  );
3090
- } catch {
3091
3388
  }
3389
+ const entry = raw;
3390
+ if (typeof entry.token !== "string" || !entry.token || !Number.isInteger(entry.pid) || entry.pid < 1 || typeof entry.createdAt !== "number" || !Number.isFinite(entry.createdAt)) {
3391
+ throw new Error(
3392
+ `invalid server start lock for ${root}: missing required fields`
3393
+ );
3394
+ }
3395
+ return {
3396
+ token: entry.token,
3397
+ pid: entry.pid,
3398
+ createdAt: entry.createdAt
3399
+ };
3400
+ }
3401
+ function acquireServerStartLock(root, now = Date.now()) {
3402
+ mkdirSync(registryDir(), { recursive: true });
3403
+ const file = serverStartLockFilePath(root);
3404
+ const token = randomUUID();
3405
+ const entry = {
3406
+ token,
3407
+ pid: process.pid,
3408
+ createdAt: now
3409
+ };
3410
+ for (let attempt = 0; attempt < 3; attempt += 1) {
3411
+ try {
3412
+ writeFileSync(file, `${JSON.stringify(entry)}
3413
+ `, {
3414
+ encoding: "utf8",
3415
+ flag: "wx",
3416
+ mode: 384
3417
+ });
3418
+ return {
3419
+ release() {
3420
+ const current2 = readServerStartLock(root);
3421
+ if (!current2 || current2.token !== token) return;
3422
+ try {
3423
+ unlinkSync(file);
3424
+ } catch (error) {
3425
+ if (errno(error) === "ENOENT") return;
3426
+ throw error;
3427
+ }
3428
+ }
3429
+ };
3430
+ } catch (error) {
3431
+ if (errno(error) !== "EEXIST") throw error;
3432
+ }
3433
+ const current = readServerStartLock(root);
3434
+ if (!current) continue;
3435
+ const stale = now - current.createdAt > SERVER_START_LOCK_STALE_MS || !processAlive(current.pid);
3436
+ if (!stale) return null;
3437
+ try {
3438
+ unlinkSync(file);
3439
+ } catch (error) {
3440
+ if (errno(error) !== "ENOENT") throw error;
3441
+ }
3442
+ }
3443
+ throw new Error(`server start lock kept changing for ${root}`);
3444
+ }
3445
+ function writeServerRegistry(entry) {
3446
+ mkdirSync(registryDir(), { recursive: true });
3447
+ writeFileSync(
3448
+ serverRegistryFilePath(entry.root),
3449
+ `${JSON.stringify(entry, null, 2)}
3450
+ `,
3451
+ "utf8"
3452
+ );
3092
3453
  }
3093
3454
  function readServerRegistry(root) {
3094
3455
  const file = serverRegistryFilePath(root);
3095
- if (!existsSync2(file)) return null;
3456
+ let raw;
3096
3457
  try {
3097
- const raw = JSON.parse(readFileSync2(file, "utf8"));
3098
- if (!raw || typeof raw !== "object") return null;
3099
- const entry = raw;
3100
- if (typeof entry.url !== "string" || !entry.url) return null;
3101
- return {
3102
- url: entry.url,
3103
- pid: typeof entry.pid === "number" ? entry.pid : 0,
3104
- root: typeof entry.root === "string" ? entry.root : root,
3105
- started_at: typeof entry.started_at === "string" ? entry.started_at : ""
3106
- };
3107
- } catch {
3108
- return null;
3458
+ raw = JSON.parse(readFileSync2(file, "utf8"));
3459
+ } catch (error) {
3460
+ if (error.code === "ENOENT") return null;
3461
+ throw errorWithCause(`failed to read server registry for ${root}`, error);
3462
+ }
3463
+ if (!raw || typeof raw !== "object") {
3464
+ throw new Error(`invalid server registry for ${root}: expected an object`);
3109
3465
  }
3466
+ const entry = raw;
3467
+ if (typeof entry.url !== "string" || !entry.url || !Number.isInteger(entry.pid) || entry.pid < 1 || typeof entry.root !== "string" || !entry.root || typeof entry.started_at !== "string" || !entry.started_at) {
3468
+ throw new Error(
3469
+ `invalid server registry for ${root}: missing required fields`
3470
+ );
3471
+ }
3472
+ return {
3473
+ url: entry.url,
3474
+ pid: entry.pid,
3475
+ root: entry.root,
3476
+ started_at: entry.started_at
3477
+ };
3110
3478
  }
3111
3479
  function removeServerRegistry(root, pid) {
3480
+ const entry = readServerRegistry(root);
3481
+ if (!entry || entry.pid !== pid) return;
3112
3482
  try {
3113
- const entry = readServerRegistry(root);
3114
- if (!entry || entry.pid !== pid) return;
3115
3483
  unlinkSync(serverRegistryFilePath(root));
3116
- } catch {
3484
+ } catch (error) {
3485
+ if (error.code === "ENOENT") return;
3486
+ throw error;
3117
3487
  }
3118
3488
  }
3489
+ var SERVER_START_LOCK_STALE_MS;
3119
3490
  var init_server_registry = __esm({
3120
3491
  "web-src/server/server-registry.ts"() {
3492
+ init_error_detail();
3493
+ SERVER_START_LOCK_STALE_MS = 3e4;
3121
3494
  }
3122
3495
  });
3123
3496
 
@@ -4322,7 +4695,7 @@ __export(file_cli_exports, {
4322
4695
  safeWorktreePathFromRoot: () => safeWorktreePathFromRoot,
4323
4696
  sliceLines: () => sliceLines
4324
4697
  });
4325
- import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
4698
+ import { existsSync as existsSync2, readFileSync as readFileSync4, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
4326
4699
  import { join as join6, relative as relative3 } from "node:path";
4327
4700
  function validatePath(value) {
4328
4701
  return validateRepoRelativePathValue(value, "--path");
@@ -4709,7 +5082,7 @@ function sliceLines(text2, start, end) {
4709
5082
  function safeWorktreePathFromRoot(root, path) {
4710
5083
  if (validatePath(path)) return null;
4711
5084
  const full = join6(root, path);
4712
- if (!existsSync3(full)) return null;
5085
+ if (!existsSync2(full)) return null;
4713
5086
  try {
4714
5087
  const realRoot = realpathSync4(root);
4715
5088
  const realFull = realpathSync4(full);
@@ -4736,8 +5109,8 @@ async function readShowTextAsync(root, command) {
4736
5109
  };
4737
5110
  }
4738
5111
  try {
4739
- const stat3 = statSync4(full);
4740
- if (!stat3.isFile()) {
5112
+ const stat4 = statSync4(full);
5113
+ if (!stat4.isFile()) {
4741
5114
  return { code: 1, stdout: "", stderr: "not a file" };
4742
5115
  }
4743
5116
  return { code: 0, stdout: readFileSync4(full, "utf8"), stderr: "" };
@@ -8907,8 +9280,8 @@ function computeFuzzyMatch(query, path) {
8907
9280
  const first = indices[0];
8908
9281
  score -= Math.min(first, 40);
8909
9282
  if (indices[0] >= baseStart) score += 20;
8910
- const basename6 = lowerPath.slice(baseStart);
8911
- const tier = pathMatchTier(q, lowerPath, basename6);
9283
+ const basename7 = lowerPath.slice(baseStart);
9284
+ const tier = pathMatchTier(q, lowerPath, basename7);
8912
9285
  const contiguousRange = contiguousPathRange(q, lowerPath, baseStart);
8913
9286
  return {
8914
9287
  score,
@@ -9060,8 +9433,8 @@ function createGlobPathMatcher(query) {
9060
9433
  const suffix = query.replace(/^\*+/, "").toLowerCase();
9061
9434
  return (path) => {
9062
9435
  const baseStart = basenameStart(path);
9063
- const basename6 = path.slice(baseStart);
9064
- if (!regex.test(path) && (query.includes("/") || !regex.test(basename6)))
9436
+ const basename7 = path.slice(baseStart);
9437
+ if (!regex.test(path) && (query.includes("/") || !regex.test(basename7)))
9065
9438
  return null;
9066
9439
  const ranges = [];
9067
9440
  const lowerPath = path.toLowerCase();
@@ -9696,13 +10069,13 @@ Parse failures and unreachable servers exit 1.
9696
10069
  });
9697
10070
 
9698
10071
  // web-src/server/root.ts
9699
- import { existsSync as existsSync4 } from "node:fs";
10072
+ import { existsSync as existsSync3 } from "node:fs";
9700
10073
  import { dirname as dirname4, join as join7, normalize } from "node:path";
9701
10074
  import { fileURLToPath } from "node:url";
9702
10075
  function findRoot(start) {
9703
10076
  let current = start;
9704
10077
  for (let i = 0; i < 5; i++) {
9705
- if (existsSync4(join7(current, "package.json")) && existsSync4(join7(current, "web"))) {
10078
+ if (existsSync3(join7(current, "package.json")) && existsSync3(join7(current, "web"))) {
9706
10079
  return normalize(current);
9707
10080
  }
9708
10081
  const parent = dirname4(current);
@@ -9728,7 +10101,7 @@ __export(skill_cli_exports, {
9728
10101
  parseSkillArgs: () => parseSkillArgs,
9729
10102
  runSkillCli: () => runSkillCli
9730
10103
  });
9731
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
10104
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs";
9732
10105
  import { homedir as homedir2 } from "node:os";
9733
10106
  import { join as join8, resolve } from "node:path";
9734
10107
  function parseAgentList(value) {
@@ -9782,9 +10155,9 @@ function parseSkillArgs(argv) {
9782
10155
  return { ok: true, args: { kind: "install", agents, global, cwd: cwd2 } };
9783
10156
  }
9784
10157
  function discoverBundledSkills(skillsRoot) {
9785
- if (!existsSync5(skillsRoot)) return [];
10158
+ if (!existsSync4(skillsRoot)) return [];
9786
10159
  return readdirSync2(skillsRoot, { withFileTypes: true }).filter(
9787
- (entry) => entry.isDirectory() && existsSync5(join8(skillsRoot, entry.name, "SKILL.md"))
10160
+ (entry) => entry.isDirectory() && existsSync4(join8(skillsRoot, entry.name, "SKILL.md"))
9788
10161
  ).map((entry) => entry.name).sort();
9789
10162
  }
9790
10163
  function installSkill(args, deps) {
@@ -9801,7 +10174,7 @@ function installSkill(args, deps) {
9801
10174
  for (const skill of skills) {
9802
10175
  const sourceDir = join8(deps.skillsRoot, skill);
9803
10176
  const target = join8(base, AGENT_SKILL_DIRS[agent], "skills", skill);
9804
- const action = existsSync5(target) ? "updated" : "installed";
10177
+ const action = existsSync4(target) ? "updated" : "installed";
9805
10178
  try {
9806
10179
  mkdirSync2(target, { recursive: true });
9807
10180
  cpSync(sourceDir, target, { recursive: true });
@@ -16244,7 +16617,7 @@ var init_control_chars = __esm({
16244
16617
  // web-src/server/database/discovery.ts
16245
16618
  import {
16246
16619
  closeSync,
16247
- existsSync as existsSync6,
16620
+ existsSync as existsSync5,
16248
16621
  openSync,
16249
16622
  readSync,
16250
16623
  realpathSync as realpathSync5,
@@ -16254,8 +16627,8 @@ import { lstat, open as open2, readdir, readFile as readFile2, stat as stat2 } f
16254
16627
  import { basename, join as join11, relative as relative5 } from "node:path";
16255
16628
  function isSqliteFile(fullPath) {
16256
16629
  try {
16257
- const stat3 = statSync5(fullPath);
16258
- if (!stat3.isFile() || stat3.size < 16) return false;
16630
+ const stat4 = statSync5(fullPath);
16631
+ if (!stat4.isFile() || stat4.size < 16) return false;
16259
16632
  const buf = Buffer.alloc(16);
16260
16633
  const fd = openSync(fullPath, "r");
16261
16634
  try {
@@ -16357,7 +16730,7 @@ function validateDbPath(cwd2, dbPath) {
16357
16730
  ))
16358
16731
  return null;
16359
16732
  const full = join11(cwd2, dbPath);
16360
- if (!existsSync6(full)) return null;
16733
+ if (!existsSync5(full)) return null;
16361
16734
  let realCwd;
16362
16735
  let realFull;
16363
16736
  try {
@@ -17050,6 +17423,36 @@ var init_discovery = __esm({
17050
17423
  }
17051
17424
  });
17052
17425
 
17426
+ // web-src/server/abort.ts
17427
+ function createLinkedAbortController(parent, timeoutMs) {
17428
+ const controller = new AbortController();
17429
+ const abortFromParent = () => controller.abort(parent?.reason);
17430
+ if (parent?.aborted) {
17431
+ abortFromParent();
17432
+ } else {
17433
+ parent?.addEventListener("abort", abortFromParent, { once: true });
17434
+ }
17435
+ const timer2 = timeoutMs === void 0 ? null : setTimeout(
17436
+ () => controller.abort(
17437
+ new Error(`operation timed out after ${timeoutMs}ms`)
17438
+ ),
17439
+ timeoutMs
17440
+ );
17441
+ timer2?.unref?.();
17442
+ return {
17443
+ signal: controller.signal,
17444
+ abort: () => controller.abort(),
17445
+ cleanup() {
17446
+ if (timer2) clearTimeout(timer2);
17447
+ parent?.removeEventListener("abort", abortFromParent);
17448
+ }
17449
+ };
17450
+ }
17451
+ var init_abort2 = __esm({
17452
+ "web-src/server/abort.ts"() {
17453
+ }
17454
+ });
17455
+
17053
17456
  // web-src/core/keymap.ts
17054
17457
  function sanitizeChord(raw) {
17055
17458
  if (!raw || typeof raw !== "object") return null;
@@ -18873,8 +19276,8 @@ async function runSecurityAsync(opts) {
18873
19276
  "refusing to run the real security binary in tests; use __setKeychainSpawnForTest"
18874
19277
  );
18875
19278
  }
18876
- const spawn4 = keychainSpawnOverride ?? spawnCollectAsync;
18877
- const result = await spawn4({
19279
+ const spawn5 = keychainSpawnOverride ?? spawnCollectAsync;
19280
+ const result = await spawn5({
18878
19281
  command: SECURITY_COMMAND,
18879
19282
  args: opts.args,
18880
19283
  ...opts.input === void 0 ? {} : { input: opts.input },
@@ -18994,7 +19397,7 @@ var init_credential_store = __esm({
18994
19397
  });
18995
19398
 
18996
19399
  // web-src/server/database/connections-store.ts
18997
- import { randomUUID } from "node:crypto";
19400
+ import { randomUUID as randomUUID2 } from "node:crypto";
18998
19401
  import { chmod } from "node:fs/promises";
18999
19402
  import { join as join13 } from "node:path";
19000
19403
  function secretKey(cwd2, id) {
@@ -19198,7 +19601,7 @@ async function findDatastoreConnection(cwd2, id) {
19198
19601
  async function saveDatastoreConnection(cwd2, raw) {
19199
19602
  const input = raw && typeof raw === "object" ? raw : {};
19200
19603
  const requestedId = typeof input.id === "string" ? input.id : "";
19201
- const id = requestedId || `connection:${randomUUID()}`;
19604
+ const id = requestedId || `connection:${randomUUID2()}`;
19202
19605
  const result = await store.update(cwd2, (state) => {
19203
19606
  const storedExisting = state.connections.find((entry) => entry.id === id);
19204
19607
  const existing = storedExisting ? withRuntimeSecrets(cwd2, storedExisting) : void 0;
@@ -22791,22 +23194,6 @@ async function handleTableCount(cwd2, url, omitDirNames, signal) {
22791
23194
  function makeHistoryId() {
22792
23195
  return makeId("qh");
22793
23196
  }
22794
- function createLinkedAbortController(parent) {
22795
- const controller = new AbortController();
22796
- const abort = () => controller.abort();
22797
- if (parent?.aborted) {
22798
- controller.abort();
22799
- } else {
22800
- parent?.addEventListener("abort", abort, { once: true });
22801
- }
22802
- return {
22803
- signal: controller.signal,
22804
- abort,
22805
- cleanup() {
22806
- parent?.removeEventListener("abort", abort);
22807
- }
22808
- };
22809
- }
22810
23197
  function unquoteSqlIdentifier(raw) {
22811
23198
  const trimmed = raw.trim();
22812
23199
  if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
@@ -24085,6 +24472,7 @@ var init_handle = __esm({
24085
24472
  "web-src/server/database/handle.ts"() {
24086
24473
  init_control_chars();
24087
24474
  init_id();
24475
+ init_abort2();
24088
24476
  init_state_store();
24089
24477
  init_abort();
24090
24478
  init_async_facade();
@@ -24564,9 +24952,549 @@ var init_command = __esm({
24564
24952
  }
24565
24953
  });
24566
24954
 
24955
+ // web-src/server/worktree/open.ts
24956
+ import { spawn as spawn3 } from "node:child_process";
24957
+ import { realpathSync as realpathSync6 } from "node:fs";
24958
+ function delay(ms) {
24959
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
24960
+ }
24961
+ function errno2(error) {
24962
+ return error.code;
24963
+ }
24964
+ function processAlive2(pid) {
24965
+ if (!Number.isInteger(pid) || pid < 1) return false;
24966
+ try {
24967
+ process.kill(pid, 0);
24968
+ return true;
24969
+ } catch (error) {
24970
+ if (errno2(error) === "ESRCH") return false;
24971
+ if (errno2(error) === "EPERM") return true;
24972
+ throw error;
24973
+ }
24974
+ }
24975
+ function registryKey(path) {
24976
+ try {
24977
+ return realpathSync6(path);
24978
+ } catch (error) {
24979
+ if (errno2(error) === "ENOENT") return path;
24980
+ throw error;
24981
+ }
24982
+ }
24983
+ function registryUrl(entry) {
24984
+ let url;
24985
+ try {
24986
+ url = new URL(entry.url);
24987
+ } catch (error) {
24988
+ throw errorWithCause("server registry contains an invalid URL", error);
24989
+ }
24990
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
24991
+ throw new Error("server registry URL must be an HTTP loopback root URL");
24992
+ }
24993
+ const port = Number(url.port);
24994
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
24995
+ throw new Error("server registry URL has an invalid port");
24996
+ }
24997
+ return url;
24998
+ }
24999
+ function settingsIdentity(value) {
25000
+ if (!value || typeof value !== "object") return null;
25001
+ const server2 = value.server;
25002
+ if (!server2 || typeof server2 !== "object") return null;
25003
+ const identity = server2;
25004
+ if (!Number.isInteger(identity.pid) || identity.pid < 1 || typeof identity.root !== "string" || !identity.root) {
25005
+ return null;
25006
+ }
25007
+ return { pid: identity.pid, root: identity.root };
25008
+ }
25009
+ async function waitForChildExit(child, timeoutMs) {
25010
+ if (child.exitCode !== null || child.signalCode !== null) return true;
25011
+ return new Promise((resolve4) => {
25012
+ const onClose = () => finish(true);
25013
+ const timer2 = setTimeout(() => finish(false), timeoutMs);
25014
+ timer2.unref?.();
25015
+ const finish = (exited) => {
25016
+ clearTimeout(timer2);
25017
+ child.off("close", onClose);
25018
+ resolve4(exited);
25019
+ };
25020
+ child.once("close", onClose);
25021
+ });
25022
+ }
25023
+ async function terminateChild(child) {
25024
+ if (child.exitCode !== null || child.signalCode !== null) return;
25025
+ child.kill("SIGTERM");
25026
+ if (await waitForChildExit(child, STOP_GRACE_MS)) return;
25027
+ child.kill("SIGKILL");
25028
+ if (!await waitForChildExit(child, STOP_GRACE_MS)) {
25029
+ throw new Error("spawned worktree server did not stop after SIGKILL");
25030
+ }
25031
+ }
25032
+ function spawnServer(path) {
25033
+ const entry = process.argv[1];
25034
+ if (!entry) throw new Error("cannot locate code-viewer entry point");
25035
+ const child = spawn3(
25036
+ process.execPath,
25037
+ [...process.execArgv, entry, "--cwd", path, "--port", "0"],
25038
+ { cwd: path, detached: true, stdio: "ignore", env: process.env }
25039
+ );
25040
+ return {
25041
+ onError(listener) {
25042
+ child.once("error", listener);
25043
+ },
25044
+ terminate: () => terminateChild(child),
25045
+ unref: () => child.unref()
25046
+ };
25047
+ }
25048
+ async function signalProcess(pid, signal) {
25049
+ try {
25050
+ process.kill(pid, signal);
25051
+ } catch (error) {
25052
+ if (errno2(error) === "ESRCH") return;
25053
+ throw error;
25054
+ }
25055
+ }
25056
+ async function terminatePid(pid) {
25057
+ await signalProcess(pid, "SIGTERM");
25058
+ const deadline = Date.now() + STOP_GRACE_MS;
25059
+ while (processAlive2(pid) && Date.now() < deadline) await delay(50);
25060
+ if (!processAlive2(pid)) return;
25061
+ await signalProcess(pid, "SIGKILL");
25062
+ const killDeadline = Date.now() + STOP_GRACE_MS;
25063
+ while (processAlive2(pid) && Date.now() < killDeadline) await delay(50);
25064
+ if (processAlive2(pid)) {
25065
+ throw new Error(`worktree server process ${pid} did not stop`);
25066
+ }
25067
+ }
25068
+ function createWorktreeServerController(overrides = {}) {
25069
+ const runtime = { ...DEFAULT_RUNTIME, ...overrides };
25070
+ const opening = /* @__PURE__ */ new Map();
25071
+ async function runningServerResult2(path, options = {}) {
25072
+ let key;
25073
+ let entry;
25074
+ try {
25075
+ key = registryKey(path);
25076
+ entry = readServerRegistry(key);
25077
+ } catch (error) {
25078
+ return { status: "invalid", error };
25079
+ }
25080
+ if (!entry) return { status: "absent" };
25081
+ if (entry.root !== key) {
25082
+ return {
25083
+ status: "invalid",
25084
+ error: new Error("server registry root does not match its file key")
25085
+ };
25086
+ }
25087
+ let url;
25088
+ try {
25089
+ url = registryUrl(entry);
25090
+ if (!processAlive2(entry.pid)) return { status: "absent" };
25091
+ } catch (error) {
25092
+ return { status: "invalid", error };
25093
+ }
25094
+ let response;
25095
+ const healthAbort = createLinkedAbortController(
25096
+ options.signal,
25097
+ options.timeoutMs ?? HEALTH_TIMEOUT_MS
25098
+ );
25099
+ try {
25100
+ response = await runtime.fetch(new URL("_settings", url).href, {
25101
+ redirect: "error",
25102
+ signal: healthAbort.signal
25103
+ });
25104
+ } catch (error) {
25105
+ return { status: "unreachable", error };
25106
+ } finally {
25107
+ healthAbort.cleanup();
25108
+ }
25109
+ if (!response.ok) {
25110
+ return {
25111
+ status: "unreachable",
25112
+ error: new Error(
25113
+ `registered server health check returned ${response.status}`
25114
+ )
25115
+ };
25116
+ }
25117
+ let body;
25118
+ try {
25119
+ body = await response.json();
25120
+ } catch (error) {
25121
+ return {
25122
+ status: "invalid",
25123
+ error: errorWithCause(
25124
+ "registered server returned invalid settings",
25125
+ error
25126
+ )
25127
+ };
25128
+ }
25129
+ const identity = settingsIdentity(body);
25130
+ if (!identity || identity.pid !== entry.pid || identity.root !== key) {
25131
+ return {
25132
+ status: "invalid",
25133
+ error: new Error(
25134
+ "registered server identity does not match the registry"
25135
+ )
25136
+ };
25137
+ }
25138
+ return { status: "running", url: url.href, pid: entry.pid };
25139
+ }
25140
+ async function spawnWhileLocked(key, deadline) {
25141
+ const existing = await runningServerResult2(key);
25142
+ if (existing.status === "running") {
25143
+ return { status: "ok", url: existing.url, started: false };
25144
+ }
25145
+ if (existing.status === "invalid" || existing.status === "unreachable") {
25146
+ return { status: "error", error: existing.error };
25147
+ }
25148
+ let child;
25149
+ try {
25150
+ child = runtime.spawnServer(key);
25151
+ } catch (error) {
25152
+ return { status: "error", error };
25153
+ }
25154
+ let spawnError = null;
25155
+ child.onError((error) => {
25156
+ spawnError = error;
25157
+ });
25158
+ child.unref();
25159
+ const stopAfterFailure = async (result) => {
25160
+ try {
25161
+ await child.terminate();
25162
+ return result;
25163
+ } catch (error) {
25164
+ const startupError = result.status === "error" ? result.error : new Error(`worktree server startup ended with ${result.status}`);
25165
+ return {
25166
+ status: "error",
25167
+ error: errorWithCauses(
25168
+ "failed to stop an unsuccessful worktree server",
25169
+ [startupError, error]
25170
+ )
25171
+ };
25172
+ }
25173
+ };
25174
+ try {
25175
+ while (runtime.now() < deadline) {
25176
+ await runtime.delay(runtime.pollIntervalMs);
25177
+ if (spawnError) {
25178
+ return stopAfterFailure({ status: "error", error: spawnError });
25179
+ }
25180
+ const found = await runningServerResult2(key);
25181
+ if (found.status === "running") {
25182
+ return { status: "ok", url: found.url, started: true };
25183
+ }
25184
+ if (found.status === "invalid") {
25185
+ return stopAfterFailure({ status: "error", error: found.error });
25186
+ }
25187
+ }
25188
+ return stopAfterFailure({ status: "timeout" });
25189
+ } catch (error) {
25190
+ return stopAfterFailure({ status: "error", error });
25191
+ }
25192
+ }
25193
+ async function doOpen(path) {
25194
+ let key;
25195
+ try {
25196
+ key = realpathSync6(path);
25197
+ } catch (error) {
25198
+ if (errno2(error) === "ENOENT") return { status: "missing" };
25199
+ return { status: "error", error };
25200
+ }
25201
+ const deadline = runtime.now() + runtime.startTimeoutMs;
25202
+ while (runtime.now() < deadline) {
25203
+ const existing = await runningServerResult2(key);
25204
+ if (existing.status === "running") {
25205
+ return { status: "ok", url: existing.url, started: false };
25206
+ }
25207
+ if (existing.status === "invalid" || existing.status === "unreachable") {
25208
+ return { status: "error", error: existing.error };
25209
+ }
25210
+ let lock;
25211
+ try {
25212
+ lock = acquireServerStartLock(key, runtime.now());
25213
+ } catch (error) {
25214
+ return { status: "error", error };
25215
+ }
25216
+ if (!lock) {
25217
+ try {
25218
+ await runtime.delay(runtime.pollIntervalMs);
25219
+ } catch (error) {
25220
+ return { status: "error", error };
25221
+ }
25222
+ continue;
25223
+ }
25224
+ let result;
25225
+ try {
25226
+ result = await spawnWhileLocked(key, deadline);
25227
+ } catch (error) {
25228
+ result = { status: "error", error };
25229
+ }
25230
+ try {
25231
+ lock.release();
25232
+ } catch (error) {
25233
+ return {
25234
+ status: "error",
25235
+ error: result.status === "error" ? errorWithCauses(
25236
+ "worktree server start and lock release both failed",
25237
+ [result.error, error]
25238
+ ) : errorWithCause(
25239
+ "worktree server start lock could not be released",
25240
+ error
25241
+ )
25242
+ };
25243
+ }
25244
+ return result;
25245
+ }
25246
+ return { status: "timeout" };
25247
+ }
25248
+ function openWorktreeServer2(path) {
25249
+ let key;
25250
+ try {
25251
+ key = registryKey(path);
25252
+ } catch (error) {
25253
+ return Promise.resolve({ status: "error", error });
25254
+ }
25255
+ const pending = opening.get(key);
25256
+ if (pending) return pending;
25257
+ const started = doOpen(path).finally(() => {
25258
+ if (opening.get(key) === started) opening.delete(key);
25259
+ });
25260
+ opening.set(key, started);
25261
+ return started;
25262
+ }
25263
+ async function stopWorktreeServer2(path) {
25264
+ const key = registryKey(path);
25265
+ const pending = opening.get(key);
25266
+ if (pending) await pending;
25267
+ const running = await runningServerResult2(key);
25268
+ if (running.status === "absent") return;
25269
+ if (running.status !== "running") throw running.error;
25270
+ await runtime.terminatePid(running.pid);
25271
+ removeServerRegistry(key, running.pid);
25272
+ }
25273
+ return { openWorktreeServer: openWorktreeServer2, runningServerResult: runningServerResult2, stopWorktreeServer: stopWorktreeServer2 };
25274
+ }
25275
+ var START_TIMEOUT_MS, POLL_INTERVAL_MS, HEALTH_TIMEOUT_MS, STOP_GRACE_MS, DEFAULT_RUNTIME, DEFAULT_CONTROLLER, openWorktreeServer, runningServerResult, stopWorktreeServer;
25276
+ var init_open = __esm({
25277
+ "web-src/server/worktree/open.ts"() {
25278
+ init_error_detail();
25279
+ init_abort2();
25280
+ init_server_registry();
25281
+ START_TIMEOUT_MS = 2e4;
25282
+ POLL_INTERVAL_MS = 150;
25283
+ HEALTH_TIMEOUT_MS = 1500;
25284
+ STOP_GRACE_MS = 2e3;
25285
+ DEFAULT_RUNTIME = {
25286
+ delay,
25287
+ fetch: (input, init) => fetch(input, init),
25288
+ now: () => Date.now(),
25289
+ pollIntervalMs: POLL_INTERVAL_MS,
25290
+ spawnServer,
25291
+ startTimeoutMs: START_TIMEOUT_MS,
25292
+ terminatePid
25293
+ };
25294
+ DEFAULT_CONTROLLER = createWorktreeServerController();
25295
+ openWorktreeServer = DEFAULT_CONTROLLER.openWorktreeServer;
25296
+ runningServerResult = DEFAULT_CONTROLLER.runningServerResult;
25297
+ stopWorktreeServer = DEFAULT_CONTROLLER.stopWorktreeServer;
25298
+ }
25299
+ });
25300
+
25301
+ // web-src/server/worktree/list.ts
25302
+ import { existsSync as existsSync6 } from "node:fs";
25303
+ import { stat as stat3 } from "node:fs/promises";
25304
+ import { basename as basename2, join as join17, relative as relative6 } from "node:path";
25305
+ async function mapWithConcurrency(items, limit, mapper) {
25306
+ if (!Number.isInteger(limit) || limit < 1) {
25307
+ throw new RangeError("concurrency limit must be a positive integer");
25308
+ }
25309
+ const results = new Array(items.length);
25310
+ let cursor = 0;
25311
+ const worker = async () => {
25312
+ while (cursor < items.length) {
25313
+ const index = cursor++;
25314
+ results[index] = await mapper(items[index], index);
25315
+ }
25316
+ };
25317
+ await Promise.all(
25318
+ Array.from(
25319
+ { length: Math.min(limit, Math.max(1, items.length)) },
25320
+ () => worker()
25321
+ )
25322
+ );
25323
+ return results;
25324
+ }
25325
+ function worktreeAddParent(repoRootPath) {
25326
+ return join17(repoRootPath, WORKTREE_ADD_DIR_NAME);
25327
+ }
25328
+ function serverWorktreeRoot(cwd2) {
25329
+ return repoRoot(cwd2) ?? cwd2;
25330
+ }
25331
+ function displayPathFor(root, path) {
25332
+ const rel = relative6(root, path);
25333
+ if (!rel) return ".";
25334
+ return rel.startsWith("..") ? path : rel;
25335
+ }
25336
+ function joinErrors(...parts) {
25337
+ return parts.filter((part) => !!part).join("\n");
25338
+ }
25339
+ function toFileChange(meta, origin) {
25340
+ return {
25341
+ path: meta.path,
25342
+ ...meta.old_path ? { oldPath: meta.old_path } : {},
25343
+ // 未追跡は git の name-status には出ないので、追加 ("A") と区別できる
25344
+ // ように専用の文字にする。
25345
+ status: meta.untracked ? "U" : meta.status || "M",
25346
+ additions: meta.additions || 0,
25347
+ deletions: meta.deletions || 0,
25348
+ origin
25349
+ };
25350
+ }
25351
+ async function collectFiles(ref, base) {
25352
+ const comparable = !!base && !!ref.branch && ref.branch !== base;
25353
+ const [uncommitted, committed] = await Promise.all([
25354
+ fileMetaResultAsync(["HEAD"], ref.path, true),
25355
+ comparable ? fileMetaResultAsync(
25356
+ [`refs/heads/${base}...refs/heads/${ref.branch}`],
25357
+ ref.path,
25358
+ false
25359
+ ) : Promise.resolve({ files: [] })
25360
+ ]);
25361
+ const files = [
25362
+ ...uncommitted.files.map((meta) => toFileChange(meta, "uncommitted")),
25363
+ ...committed.files.map((meta) => toFileChange(meta, "committed"))
25364
+ ];
25365
+ return {
25366
+ files,
25367
+ error: joinErrors(uncommitted.error, committed.error) || void 0
25368
+ };
25369
+ }
25370
+ async function lastTouchedMs(ref, files) {
25371
+ const targets = files.filter((file) => file.status !== "D");
25372
+ const errors = [];
25373
+ const mtimes = await mapWithConcurrency(targets, 8, async (file) => {
25374
+ try {
25375
+ return (await stat3(join17(ref.path, file.path))).mtimeMs;
25376
+ } catch (error) {
25377
+ if (error.code !== "ENOENT") {
25378
+ errors.push(formatErrorDetail(error));
25379
+ }
25380
+ return null;
25381
+ }
25382
+ });
25383
+ const valid = mtimes.filter((value) => value !== null);
25384
+ return {
25385
+ at: valid.length ? Math.max(...valid) : null,
25386
+ ...errors.length ? { error: errors.join("\n") } : {}
25387
+ };
25388
+ }
25389
+ async function collectDivergence(root, ref, base) {
25390
+ if (!base || !ref.branch || ref.branch === base) {
25391
+ return { divergence: null };
25392
+ }
25393
+ const [counts, merge] = await Promise.all([
25394
+ worktreeDivergenceResultAsync(root, base, ref.branch),
25395
+ mergePreviewResultAsync(root, base, ref.branch)
25396
+ ]);
25397
+ if (counts.error) return { divergence: null, error: counts.error };
25398
+ return {
25399
+ divergence: {
25400
+ base,
25401
+ ahead: counts.ahead,
25402
+ behind: counts.behind,
25403
+ mergeState: merge.state,
25404
+ conflicts: merge.conflicts
25405
+ },
25406
+ error: merge.error
25407
+ };
25408
+ }
25409
+ function emptyItem(root, ref) {
25410
+ return {
25411
+ ...ref,
25412
+ id: ref.path,
25413
+ name: basename2(ref.path) || ref.path,
25414
+ displayPath: displayPathFor(root, ref.path),
25415
+ current: ref.path === root,
25416
+ missing: !existsSync6(ref.path),
25417
+ changedCount: 0,
25418
+ error: "",
25419
+ lastCommit: null,
25420
+ lastTouched: null,
25421
+ serverUrl: "",
25422
+ divergence: null,
25423
+ files: [],
25424
+ fileCount: 0
25425
+ };
25426
+ }
25427
+ async function buildItem(root, base, ref) {
25428
+ const item = emptyItem(root, ref);
25429
+ if (item.missing || ref.bare) return item;
25430
+ const [history, changes, divergence, server2] = await Promise.all([
25431
+ commitHistoryAsync(ref.path, { ref: "HEAD", skip: 0, limit: 1 }),
25432
+ collectFiles(ref, base),
25433
+ collectDivergence(root, ref, base),
25434
+ runningServerResult(ref.path)
25435
+ ]);
25436
+ const commit = history.commits[0];
25437
+ const touched = await lastTouchedMs(ref, changes.files);
25438
+ return {
25439
+ ...item,
25440
+ changedCount: changes.files.filter((file) => file.origin === "uncommitted").length,
25441
+ error: joinErrors(
25442
+ changes.error,
25443
+ divergence.error,
25444
+ history.error,
25445
+ touched.error,
25446
+ server2.status === "invalid" || server2.status === "unreachable" ? formatErrorDetail(server2.error) : void 0
25447
+ ),
25448
+ lastCommit: commit ? {
25449
+ sha: commit.sha,
25450
+ subject: commit.subject,
25451
+ author: commit.author,
25452
+ when: commit.when
25453
+ } : null,
25454
+ // 変更が 1 つも無い作業ツリーは、最後のコミットの時刻に落とす。
25455
+ lastTouched: touched.at !== null ? new Date(touched.at).toISOString() : commit?.when ?? null,
25456
+ serverUrl: server2.status === "running" ? server2.url : "",
25457
+ divergence: divergence.divergence,
25458
+ files: changes.files,
25459
+ fileCount: changes.files.length
25460
+ };
25461
+ }
25462
+ async function buildWorktreeList(root) {
25463
+ const [listed, baseResult] = await Promise.all([
25464
+ worktreeListResultAsync(root),
25465
+ defaultBranchResultAsync(root)
25466
+ ]);
25467
+ const baseBranch = baseResult.branch;
25468
+ const worktrees = await mapWithConcurrency(
25469
+ listed.worktrees,
25470
+ WORKTREE_LIST_CONCURRENCY,
25471
+ (ref) => buildItem(root, baseBranch, ref)
25472
+ );
25473
+ const error = joinErrors(listed.error, baseResult.error);
25474
+ return {
25475
+ worktrees,
25476
+ repoRoot: root,
25477
+ addParent: worktreeAddParent(root),
25478
+ baseBranch,
25479
+ overlaps: findWorktreeOverlaps(worktrees),
25480
+ ...error ? { error } : {}
25481
+ };
25482
+ }
25483
+ var WORKTREE_ADD_DIR_NAME, WORKTREE_LIST_CONCURRENCY;
25484
+ var init_list = __esm({
25485
+ "web-src/server/worktree/list.ts"() {
25486
+ init_error_detail();
25487
+ init_worktree();
25488
+ init_git();
25489
+ init_open();
25490
+ WORKTREE_ADD_DIR_NAME = ".worktrees";
25491
+ WORKTREE_LIST_CONCURRENCY = 3;
25492
+ }
25493
+ });
25494
+
24567
25495
  // web-src/server/doctor.ts
24568
25496
  import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync6, statSync as statSync6 } from "node:fs";
24569
- import { dirname as dirname5, join as join17, relative as relative6 } from "node:path";
25497
+ import { dirname as dirname5, join as join18, relative as relative7 } from "node:path";
24570
25498
  import { fileURLToPath as fileURLToPath2 } from "node:url";
24571
25499
  function statusWorse(a, b) {
24572
25500
  const rank = { ok: 0, warn: 1, error: 2 };
@@ -24668,7 +25596,7 @@ function findCodeViewerPackageJson() {
24668
25596
  cursor = dirname5(process.argv[1] || ".");
24669
25597
  }
24670
25598
  for (let depth = 0; depth < 8; depth += 1) {
24671
- const candidate = join17(cursor, "package.json");
25599
+ const candidate = join18(cursor, "package.json");
24672
25600
  try {
24673
25601
  const raw = readFileSync6(candidate, "utf8");
24674
25602
  const pkg = JSON.parse(raw);
@@ -24765,7 +25693,7 @@ async function checkSqlite(cwd2) {
24765
25693
  return { id: "sqlite", title: "SQLite driver", rows };
24766
25694
  }
24767
25695
  async function trySnapshotDbOpen(cwd2) {
24768
- const dbPath = join17(cwd2, SNAPSHOT_DB_REL);
25696
+ const dbPath = join18(cwd2, SNAPSHOT_DB_REL);
24769
25697
  try {
24770
25698
  statSync6(dbPath);
24771
25699
  } catch {
@@ -24787,7 +25715,7 @@ async function trySnapshotDbOpen(cwd2) {
24787
25715
  }
24788
25716
  }
24789
25717
  function checkSnapshotStore(cwd2) {
24790
- const dbPath = join17(cwd2, SNAPSHOT_DB_REL);
25718
+ const dbPath = join18(cwd2, SNAPSHOT_DB_REL);
24791
25719
  const dir = dirname5(dbPath);
24792
25720
  let dirStatus = "ok";
24793
25721
  let dirDetail = dir;
@@ -24807,8 +25735,8 @@ function checkSnapshotStore(cwd2) {
24807
25735
  }
24808
25736
  let dbDetail = dbPath;
24809
25737
  try {
24810
- const stat3 = statSync6(dbPath);
24811
- dbDetail = `${dbPath} (${stat3.size.toLocaleString()} bytes)`;
25738
+ const stat4 = statSync6(dbPath);
25739
+ dbDetail = `${dbPath} (${stat4.size.toLocaleString()} bytes)`;
24812
25740
  } catch {
24813
25741
  dbDetail = `${dbPath} (not created yet — created on first snapshot)`;
24814
25742
  }
@@ -24884,7 +25812,7 @@ async function checkGit(cwd2, signal) {
24884
25812
  cwd2
24885
25813
  );
24886
25814
  const top = topRes?.stdout.trim() || cwd2;
24887
- const insideCwd = relative6(top, cwd2) || ".";
25815
+ const insideCwd = relative7(top, cwd2) || ".";
24888
25816
  rows.push({
24889
25817
  id: "git.repo",
24890
25818
  title: "Working tree",
@@ -25707,7 +26635,7 @@ async function checkDatastoreConnectivity(cwd2, omitDirNames, signal, deps = DEF
25707
26635
  });
25708
26636
  return { id: "datastore", title: "Datastore connectivity", rows };
25709
26637
  }
25710
- function checkServer(listenPort2) {
26638
+ async function checkServer(listenPort2, cwd2, signal) {
25711
26639
  const rows = [
25712
26640
  {
25713
26641
  id: "server.port",
@@ -25716,6 +26644,49 @@ function checkServer(listenPort2) {
25716
26644
  detail: listenPort2 > 0 ? `http://localhost:${listenPort2}/` : "port not yet bound"
25717
26645
  }
25718
26646
  ];
26647
+ const root = serverWorktreeRoot(cwd2);
26648
+ const listed = await worktreeListResultAsync(root, {
26649
+ timeout: TIMEOUT.git,
26650
+ signal
26651
+ });
26652
+ if (listed.error) {
26653
+ rows.push({
26654
+ id: "server.worktrees",
26655
+ title: "Worktree servers",
26656
+ status: "warn",
26657
+ detail: `could not list worktrees: ${listed.error}`
26658
+ });
26659
+ return { id: "server", title: "Server", rows };
26660
+ }
26661
+ const checked = await mapWithConcurrency(
26662
+ listed.worktrees.filter((entry) => entry.path !== root),
26663
+ WORKTREE_LIST_CONCURRENCY,
26664
+ async (entry) => ({
26665
+ path: entry.path,
26666
+ server: await runningServerResult(entry.path, {
26667
+ signal,
26668
+ timeoutMs: TIMEOUT.git
26669
+ })
26670
+ })
26671
+ );
26672
+ const running = checked.flatMap(
26673
+ (entry) => entry.server.status === "running" ? [{ path: entry.path, url: entry.server.url }] : []
26674
+ );
26675
+ const serverErrors = checked.flatMap(
26676
+ (entry) => entry.server.status === "invalid" || entry.server.status === "unreachable" ? [`${entry.path}: ${formatErrorDetail(entry.server.error)}`] : []
26677
+ );
26678
+ rows.push({
26679
+ id: "server.worktrees",
26680
+ title: "Worktree servers",
26681
+ status: serverErrors.length ? "warn" : "ok",
26682
+ detail: [
26683
+ ...running.length ? running.map((entry) => `${entry.url} ${entry.path}`) : ["no other code-viewer is running for this repository's worktrees"],
26684
+ ...serverErrors
26685
+ ].join("\n"),
26686
+ ...running.length ? {
26687
+ hint: "stop one with `kill <pid>`; the pid is in ~/.cache/code-viewer/servers/"
26688
+ } : {}
26689
+ });
25719
26690
  return { id: "server", title: "Server", rows };
25720
26691
  }
25721
26692
  async function buildDoctorReport(ctx) {
@@ -25740,7 +26711,7 @@ async function buildDoctorReport(ctx) {
25740
26711
  ctx.signal
25741
26712
  );
25742
26713
  const terminal = await checkTerminalTools(ctx.signal);
25743
- const server2 = checkServer(ctx.listenPort);
26714
+ const server2 = await checkServer(ctx.listenPort, ctx.cwd, ctx.signal);
25744
26715
  const groups = [
25745
26716
  runtime,
25746
26717
  packageGroup,
@@ -25784,6 +26755,7 @@ async function handleDoctor(ctx) {
25784
26755
  var SNAPSHOT_DB_REL, REQUIRED_NODE_MAJOR, TTL, TIMEOUT, versionCache, gitCache, dockerInfoCache, CACHE_KEY_SEP, composeConfigCache, composePsCache2, doctorGeneration, DEFAULT_DATASTORE_PROBE_TIMEOUT_MS, DEFAULT_DATASTORE_CONNECTIVITY_DEPS;
25785
26756
  var init_doctor = __esm({
25786
26757
  "web-src/server/doctor.ts"() {
26758
+ init_error_detail();
25787
26759
  init_cli_helpers();
25788
26760
  init_command_resolver();
25789
26761
  init_docker();
@@ -25795,8 +26767,11 @@ var init_doctor = __esm({
25795
26767
  init_discovery();
25796
26768
  init_handle();
25797
26769
  init_sqlite_driver();
26770
+ init_git();
25798
26771
  init_session();
25799
26772
  init_command();
26773
+ init_list();
26774
+ init_open();
25800
26775
  SNAPSHOT_DB_REL = ".code-viewer/db-snapshots.sqlite";
25801
26776
  REQUIRED_NODE_MAJOR = 20;
25802
26777
  TTL = {
@@ -26023,7 +26998,7 @@ var init_directory_name = __esm({
26023
26998
  });
26024
26999
 
26025
27000
  // web-src/server/dev-assets.ts
26026
- import { basename as basename2 } from "node:path";
27001
+ import { basename as basename3 } from "node:path";
26027
27002
  function startDevAssetReload(options) {
26028
27003
  if (!options.enabled) return false;
26029
27004
  const watched = new Set(options.watchedFiles);
@@ -26032,7 +27007,7 @@ function startDevAssetReload(options) {
26032
27007
  const debounceMs = options.debounceMs ?? 150;
26033
27008
  let timer2 = null;
26034
27009
  options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
26035
- if (!filename || !watched.has(basename2(filename.toString()))) return;
27010
+ if (!filename || !watched.has(basename3(filename.toString()))) return;
26036
27011
  if (timer2) clearTimer(timer2);
26037
27012
  timer2 = setTimer(() => {
26038
27013
  timer2 = null;
@@ -26143,12 +27118,12 @@ var init_file_upload = __esm({
26143
27118
  });
26144
27119
 
26145
27120
  // web-src/server/journal.ts
26146
- import { join as join18 } from "node:path";
27121
+ import { join as join19 } from "node:path";
26147
27122
  function dailyJournalFilePath(root) {
26148
- return join18(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
27123
+ return join19(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
26149
27124
  }
26150
27125
  function journalTasksFilePath(root) {
26151
- return join18(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
27126
+ return join19(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
26152
27127
  }
26153
27128
  function emptyDailyJournalState() {
26154
27129
  return { version: 1, entries: [] };
@@ -26738,9 +27713,9 @@ var init_file_filter = __esm({
26738
27713
  });
26739
27714
 
26740
27715
  // web-src/server/search-service.ts
26741
- import { existsSync as existsSync7, realpathSync as realpathSync6 } from "node:fs";
27716
+ import { existsSync as existsSync7, realpathSync as realpathSync7 } from "node:fs";
26742
27717
  import { lstat as lstat2, readFile as readFile3 } from "node:fs/promises";
26743
- import { join as join19, relative as relative7 } from "node:path";
27718
+ import { join as join20, relative as relative8 } from "node:path";
26744
27719
  async function rgAvailableAsync(cwd2) {
26745
27720
  if (rgAvailableCache !== null) return rgAvailableCache;
26746
27721
  const proc = await spawnTextAsync({
@@ -26767,17 +27742,17 @@ function isSafePath(path) {
26767
27742
  function safeWorktreePath(env, path) {
26768
27743
  if (!isSafePath(path)) return null;
26769
27744
  if (isGitInternalPath(path)) return null;
26770
- const full = join19(env.cwd, path);
27745
+ const full = join20(env.cwd, path);
26771
27746
  if (!existsSync7(full)) return null;
26772
27747
  let realCwd;
26773
27748
  let realFull;
26774
27749
  try {
26775
- realCwd = realpathSync6(env.cwd);
26776
- realFull = realpathSync6(full);
27750
+ realCwd = realpathSync7(env.cwd);
27751
+ realFull = realpathSync7(full);
26777
27752
  } catch {
26778
27753
  return null;
26779
27754
  }
26780
- const rel = relative7(realCwd, realFull);
27755
+ const rel = relative8(realCwd, realFull);
26781
27756
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
26782
27757
  return null;
26783
27758
  if (isGitInternalPath(rel)) return null;
@@ -26801,13 +27776,13 @@ async function grepWorktreeFallback(env, query, max, paths, excludeTests) {
26801
27776
  continue;
26802
27777
  const full = safeWorktreePath(env, path);
26803
27778
  if (!full) continue;
26804
- let stat3;
27779
+ let stat4;
26805
27780
  try {
26806
- stat3 = await lstat2(full);
27781
+ stat4 = await lstat2(full);
26807
27782
  } catch {
26808
27783
  continue;
26809
27784
  }
26810
- if (!stat3.isFile() || stat3.isSymbolicLink() || stat3.size > GREP_MAX_FILE_BYTES)
27785
+ if (!stat4.isFile() || stat4.isSymbolicLink() || stat4.size > GREP_MAX_FILE_BYTES)
26811
27786
  continue;
26812
27787
  let data;
26813
27788
  try {
@@ -28059,7 +29034,7 @@ var init_agent_state2 = __esm({
28059
29034
  });
28060
29035
 
28061
29036
  // web-src/server/terminal/rules.ts
28062
- import { join as join20 } from "node:path";
29037
+ import { join as join21 } from "node:path";
28063
29038
  function errorIssue(code, error) {
28064
29039
  return {
28065
29040
  path: "$",
@@ -28072,7 +29047,7 @@ function defaultResponse(errors = []) {
28072
29047
  return { rules: DEFAULT_AGENT_SCREEN_RULES, source: "default", errors };
28073
29048
  }
28074
29049
  function agentScreenRulesFilePath(root) {
28075
- return join20(root, ".code-viewer", RULES_FILE_NAME);
29050
+ return join21(root, ".code-viewer", RULES_FILE_NAME);
28076
29051
  }
28077
29052
  function parseStoredRules(raw) {
28078
29053
  const parsed = parseAgentScreenRuleSet(raw);
@@ -28421,7 +29396,7 @@ var init_capture2 = __esm({
28421
29396
 
28422
29397
  // web-src/server/mcp.ts
28423
29398
  import { readFileSync as readFileSync7 } from "node:fs";
28424
- import { join as join21 } from "node:path";
29399
+ import { join as join22 } from "node:path";
28425
29400
  function defaultMcpTools(options = {}) {
28426
29401
  return [
28427
29402
  {
@@ -30175,7 +31150,7 @@ var init_mcp = __esm({
30175
31150
  init_capture();
30176
31151
  MCP_PROTOCOL_VERSION = "2025-06-18";
30177
31152
  PACKAGE_VERSION = JSON.parse(
30178
- readFileSync7(join21(ROOT, "package.json"), "utf8")
31153
+ readFileSync7(join22(ROOT, "package.json"), "utf8")
30179
31154
  ).version;
30180
31155
  MCP_SERVER_INFO = {
30181
31156
  name: "code-viewer",
@@ -30364,10 +31339,10 @@ var init_os_opener = __esm({
30364
31339
  });
30365
31340
 
30366
31341
  // web-src/server/os-trash.ts
30367
- import { randomUUID as randomUUID2 } from "node:crypto";
31342
+ import { randomUUID as randomUUID3 } from "node:crypto";
30368
31343
  import { existsSync as existsSync8, lstatSync as lstatSync5, mkdirSync as mkdirSync4, renameSync } from "node:fs";
30369
31344
  import { homedir as homedir3, release as osRelease2 } from "node:os";
30370
- import { basename as basename3, dirname as dirname6, join as join22, resolve as resolve2 } from "node:path";
31345
+ import { basename as basename4, dirname as dirname6, join as join23, resolve as resolve2 } from "node:path";
30371
31346
  function windowsTrashScript(path) {
30372
31347
  const quotedPath = path.replace(/'/g, "''");
30373
31348
  return [
@@ -30458,12 +31433,12 @@ async function runRequiredCommand(operation, command, cwd2) {
30458
31433
  }
30459
31434
  }
30460
31435
  function managedTrashRoot(cwd2) {
30461
- return join22(cwd2, ".code-viewer", "trash");
31436
+ return join23(cwd2, ".code-viewer", "trash");
30462
31437
  }
30463
31438
  function movePathIntoTrashDirectory(path, trashRoot) {
30464
31439
  mkdirSync4(trashRoot, { recursive: true });
30465
- const name = basename3(path) || "trash-item";
30466
- const trashPath = join22(trashRoot, `${name}-${randomUUID2()}`);
31440
+ const name = basename4(path) || "trash-item";
31441
+ const trashPath = join23(trashRoot, `${name}-${randomUUID3()}`);
30467
31442
  if (existsSync8(trashPath)) {
30468
31443
  throw Object.assign(new Error("trash destination already exists"), {
30469
31444
  trashPath
@@ -30473,7 +31448,7 @@ function movePathIntoTrashDirectory(path, trashRoot) {
30473
31448
  return { trashPath };
30474
31449
  }
30475
31450
  function trashRootForHandle(cwd2, platform, release) {
30476
- if (platform === "darwin") return join22(homedir3(), ".Trash");
31451
+ if (platform === "darwin") return join23(homedir3(), ".Trash");
30477
31452
  if (isWsl(platform, release)) return managedTrashRoot(cwd2);
30478
31453
  return null;
30479
31454
  }
@@ -30486,7 +31461,7 @@ function unsupportedTrashError(operation, platform, release) {
30486
31461
  async function movePathToTrash(path, cwd2, platform = process.platform, release = osRelease2()) {
30487
31462
  lstatSync5(path);
30488
31463
  if (platform === "darwin") {
30489
- return movePathIntoTrashDirectory(path, join22(homedir3(), ".Trash"));
31464
+ return movePathIntoTrashDirectory(path, join23(homedir3(), ".Trash"));
30490
31465
  }
30491
31466
  if (isWsl(platform, release)) {
30492
31467
  return movePathIntoTrashDirectory(path, managedTrashRoot(cwd2));
@@ -30574,18 +31549,18 @@ var init_request_origin = __esm({
30574
31549
  });
30575
31550
 
30576
31551
  // web-src/server/watch-supervisor.ts
30577
- import { spawn as spawn3 } from "node:child_process";
30578
- import { join as join23 } from "node:path";
31552
+ import { spawn as spawn4 } from "node:child_process";
31553
+ import { join as join24 } from "node:path";
30579
31554
  import { fileURLToPath as fileURLToPath3 } from "node:url";
30580
31555
  function watchChildCommand() {
30581
31556
  const entry = process.argv[1] ?? "";
30582
31557
  const isTypeScriptEntry = entry.endsWith(".ts");
30583
- const script = isTypeScriptEntry ? join23(fileURLToPath3(new URL(".", import.meta.url)), "cli.ts") : entry;
31558
+ const script = isTypeScriptEntry ? join24(fileURLToPath3(new URL(".", import.meta.url)), "cli.ts") : entry;
30584
31559
  const loaderArgs = isTypeScriptEntry ? process.execArgv : [];
30585
31560
  return [process.argv[0], ...loaderArgs, script, "watch-child"];
30586
31561
  }
30587
31562
  function startWatchSupervisor(options) {
30588
- const spawnChild = options.spawnFn || spawn3;
31563
+ const spawnChild = options.spawnFn || spawn4;
30589
31564
  const now = options.nowFn || Date.now;
30590
31565
  const setTimer = options.setTimeoutFn || setTimeout;
30591
31566
  const setRepeating = options.setIntervalFn || setInterval;
@@ -30874,7 +31849,7 @@ async function openTmuxPaneInShell(paneId, cwd2, size = {}) {
30874
31849
  }
30875
31850
  return { status: "ok", session: created.session, action: "attached" };
30876
31851
  }
30877
- var init_open = __esm({
31852
+ var init_open2 = __esm({
30878
31853
  "web-src/server/terminal/open.ts"() {
30879
31854
  init_error_detail();
30880
31855
  init_session();
@@ -30952,7 +31927,7 @@ var init_handle2 = __esm({
30952
31927
  init_error_detail();
30953
31928
  init_tmux();
30954
31929
  init_handle_shared();
30955
- init_open();
31930
+ init_open2();
30956
31931
  init_clients();
30957
31932
  init_panes();
30958
31933
  }
@@ -31176,9 +32151,9 @@ var init_handle3 = __esm({
31176
32151
  });
31177
32152
 
31178
32153
  // web-src/server/terminal/images.ts
31179
- import { realpathSync as realpathSync7, statSync as statSync7 } from "node:fs";
32154
+ import { realpathSync as realpathSync8, statSync as statSync7 } from "node:fs";
31180
32155
  import { homedir as homedir4 } from "node:os";
31181
- import { basename as basename4, isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
32156
+ import { basename as basename5, isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
31182
32157
  function resolveTerminalImage(cwd2, candidate) {
31183
32158
  if (typeof candidate !== "string" || candidate === "") return null;
31184
32159
  if (candidate.includes("\0")) return null;
@@ -31186,11 +32161,11 @@ function resolveTerminalImage(cwd2, candidate) {
31186
32161
  const expanded = candidate.startsWith("~/") ? resolve3(homedir4(), candidate.slice(2)) : candidate;
31187
32162
  const full = isAbsolute2(expanded) ? expanded : resolve3(cwd2, expanded);
31188
32163
  try {
31189
- const real = realpathSync7(full);
31190
- const stat3 = statSync7(real);
31191
- if (!stat3.isFile()) return null;
31192
- if (stat3.size === 0 || stat3.size > MAX_IMAGE_BYTES) return null;
31193
- return { path: real, bytes: stat3.size };
32164
+ const real = realpathSync8(full);
32165
+ const stat4 = statSync7(real);
32166
+ if (!stat4.isFile()) return null;
32167
+ if (stat4.size === 0 || stat4.size > MAX_IMAGE_BYTES) return null;
32168
+ return { path: real, bytes: stat4.size };
31194
32169
  } catch {
31195
32170
  return null;
31196
32171
  }
@@ -31209,7 +32184,7 @@ function resolveTerminalImages(cwd2, candidates) {
31209
32184
  path: image.path,
31210
32185
  // 画面で探すのは、渡された綴りそのもの。実体のパスとは違うことがある。
31211
32186
  candidate,
31212
- name: basename4(image.path),
32187
+ name: basename5(image.path),
31213
32188
  url: terminalImageUrl(image.path)
31214
32189
  });
31215
32190
  }
@@ -31226,7 +32201,7 @@ var init_images = __esm({
31226
32201
 
31227
32202
  // web-src/server/terminal/paste.ts
31228
32203
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
31229
- import { join as join24 } from "node:path";
32204
+ import { join as join25 } from "node:path";
31230
32205
  async function savePastedImage(cwd2, mime, base64) {
31231
32206
  const extension = pasteImageExtension(mime);
31232
32207
  if (!extension) {
@@ -31251,8 +32226,8 @@ async function savePastedImage(cwd2, mime, base64) {
31251
32226
  return { status: "invalid", message: "image too large" };
31252
32227
  }
31253
32228
  const name = `${makeTimedId("paste")}.${extension}`;
31254
- const dir = join24(cwd2, PASTE_DIR);
31255
- const path = join24(dir, name);
32229
+ const dir = join25(cwd2, PASTE_DIR);
32230
+ const path = join25(dir, name);
31256
32231
  try {
31257
32232
  await mkdir2(dir, { recursive: true });
31258
32233
  await writeFile2(path, bytes);
@@ -31269,7 +32244,7 @@ var init_paste = __esm({
31269
32244
  "web-src/server/terminal/paste.ts"() {
31270
32245
  init_id();
31271
32246
  init_terminal_paste();
31272
- PASTE_DIR = join24(".code-viewer", "pasted");
32247
+ PASTE_DIR = join25(".code-viewer", "pasted");
31273
32248
  }
31274
32249
  });
31275
32250
 
@@ -31476,6 +32451,260 @@ var init_handle4 = __esm({
31476
32451
  }
31477
32452
  });
31478
32453
 
32454
+ // web-src/server/worktree/handle.ts
32455
+ var handle_exports5 = {};
32456
+ __export(handle_exports5, {
32457
+ handleWorktreeRoute: () => handleWorktreeRoute
32458
+ });
32459
+ import { existsSync as existsSync9 } from "node:fs";
32460
+ import { join as join26 } from "node:path";
32461
+ function actionJson(body) {
32462
+ return json(body);
32463
+ }
32464
+ function bodyString(body, key) {
32465
+ const value = body[key];
32466
+ return typeof value === "string" ? value.trim() : "";
32467
+ }
32468
+ async function handleListGet(cwd2, generation2) {
32469
+ const list = await buildWorktreeList(serverWorktreeRoot(cwd2));
32470
+ const response = { ...list, generation: generation2 };
32471
+ return json(response);
32472
+ }
32473
+ async function resolveListedPath(cwd2, path) {
32474
+ const root = serverWorktreeRoot(cwd2);
32475
+ if (!path) return textError("path is required", 400);
32476
+ const listed = await worktreeListResultAsync(root);
32477
+ if (listed.error) return textError(listed.error, listed.status ?? 500);
32478
+ const found = findWorktree(listed.worktrees, path);
32479
+ if (!found) return textError("unknown worktree", 404);
32480
+ return {
32481
+ path: found.path,
32482
+ root,
32483
+ branch: found.branch,
32484
+ current: found.path === root,
32485
+ ref: found
32486
+ };
32487
+ }
32488
+ async function handleDiffGet(url, cwd2, generation2) {
32489
+ const file = url.searchParams.get("file") || "";
32490
+ if (!file) return textError("file is required", 400);
32491
+ if (!isSafePath(file)) return textError("invalid file path", 400);
32492
+ if (file.startsWith("-")) return textError("invalid file path", 400);
32493
+ if (isGitInternalPath(file)) return textError("forbidden", 403);
32494
+ const resolved = await resolveListedPath(
32495
+ cwd2,
32496
+ url.searchParams.get("path") || ""
32497
+ );
32498
+ if (resolved instanceof Response) return resolved;
32499
+ const origin = url.searchParams.get("origin") === "committed" ? "committed" : "uncommitted";
32500
+ const untracked = url.searchParams.get("untracked") === "1";
32501
+ const extras = [];
32502
+ if (url.searchParams.get("ignore_ws") === "1") extras.push("-w");
32503
+ if (url.searchParams.get("ignore_blank") === "1") {
32504
+ extras.push("--ignore-blank-lines");
32505
+ }
32506
+ const baseResult = origin === "committed" ? await defaultBranchResultAsync(resolved.root) : { branch: "" };
32507
+ if (baseResult.error) {
32508
+ return textError(baseResult.error, baseResult.status ?? 500);
32509
+ }
32510
+ const base = baseResult.branch;
32511
+ let args = ["HEAD"];
32512
+ if (origin === "committed") {
32513
+ if (!base || !resolved.branch || base === resolved.branch) {
32514
+ return textError("nothing to compare against", 409);
32515
+ }
32516
+ args = [`refs/heads/${base}...refs/heads/${resolved.branch}`];
32517
+ }
32518
+ const changes = await collectFiles(resolved.ref, base);
32519
+ const listedFile = changes.files.find(
32520
+ (change) => change.path === file && change.origin === origin
32521
+ );
32522
+ if (!listedFile) return textError("file is not changed here", 404);
32523
+ if (!safeWorktreePathFromRoot(resolved.path, file)) {
32524
+ return textError("forbidden", 403);
32525
+ }
32526
+ const res = untracked ? await untrackedFileDiffAsync(extras, file, resolved.path) : await fileDiffTextAsync([...extras, ...args], file, resolved.path);
32527
+ const failed = untracked ? res.code > 1 : res.code !== 0;
32528
+ if (failed) {
32529
+ return textError(res.stderr.trim() || "git diff failed", res.status ?? 500);
32530
+ }
32531
+ const truncated = truncateToNHunks(
32532
+ res.stdout,
32533
+ DIFF_MAX_HUNKS,
32534
+ DIFF_MAX_LINES
32535
+ );
32536
+ const response = {
32537
+ file,
32538
+ origin,
32539
+ diff: truncated.text,
32540
+ totalHunks: truncated.totalHunks,
32541
+ renderedHunks: truncated.renderedHunks,
32542
+ truncated: truncated.renderedHunks < truncated.totalHunks,
32543
+ generation: generation2
32544
+ };
32545
+ return json(response);
32546
+ }
32547
+ async function handleAddPost(req, cwd2) {
32548
+ const parsed = await parseBoundedJsonBody(
32549
+ req,
32550
+ BODY_MAX_BYTES,
32551
+ "body too large"
32552
+ );
32553
+ if (parsed instanceof Response) return parsed;
32554
+ const body = parsed ?? {};
32555
+ const name = bodyString(body, "name");
32556
+ const nameError = worktreeNameError(name);
32557
+ if (nameError) return textError(`invalid name: ${nameError}`, 400);
32558
+ const branch = bodyString(body, "branch") || name;
32559
+ const branchError = worktreeBranchError(branch);
32560
+ if (branchError) return textError(`invalid branch: ${branchError}`, 400);
32561
+ const root = serverWorktreeRoot(cwd2);
32562
+ const path = join26(worktreeAddParent(root), name);
32563
+ if (existsSync9(path)) return textError("path already exists", 409);
32564
+ const exists = await localBranchExistsResultAsync(root, branch);
32565
+ if (exists.error) return textError(exists.error, exists.status ?? 500);
32566
+ const result = await worktreeAddResultAsync(root, {
32567
+ path,
32568
+ branch,
32569
+ createBranch: !exists.exists
32570
+ });
32571
+ if (result.error) return textError(result.error, result.status ?? 500);
32572
+ return actionJson({ path });
32573
+ }
32574
+ async function handleRemovePost(req, cwd2) {
32575
+ const parsed = await parseBoundedJsonBody(
32576
+ req,
32577
+ BODY_MAX_BYTES,
32578
+ "body too large"
32579
+ );
32580
+ if (parsed instanceof Response) return parsed;
32581
+ const body = parsed ?? {};
32582
+ const resolved = await resolveListedPath(cwd2, bodyString(body, "path"));
32583
+ if (resolved instanceof Response) return resolved;
32584
+ if (resolved.current) {
32585
+ return textError("cannot remove the worktree this server is serving", 409);
32586
+ }
32587
+ const pruning = !existsSync9(resolved.path);
32588
+ const result = pruning ? await worktreePruneResultAsync(resolved.root) : await worktreeRemoveResultAsync(resolved.root, {
32589
+ path: resolved.path,
32590
+ force: body.force === true
32591
+ });
32592
+ if (result.error) return textError(result.error, result.status ?? 500);
32593
+ if (pruning) {
32594
+ const after = await worktreeListResultAsync(resolved.root);
32595
+ if (after.error) return textError(after.error, after.status ?? 500);
32596
+ if (findWorktree(after.worktrees, resolved.path)) {
32597
+ return textError(
32598
+ resolved.ref.locked ? "the entry is locked, so git worktree prune left it in place; unlock it first" : "git worktree prune left the entry in place",
32599
+ 409
32600
+ );
32601
+ }
32602
+ }
32603
+ try {
32604
+ await stopWorktreeServer(resolved.path);
32605
+ } catch (error) {
32606
+ throw errorWithCause(
32607
+ "worktree was removed but its code-viewer server could not be stopped",
32608
+ error
32609
+ );
32610
+ }
32611
+ return actionJson({});
32612
+ }
32613
+ async function handleStopPost(req, cwd2) {
32614
+ const parsed = await parseBoundedJsonBody(
32615
+ req,
32616
+ BODY_MAX_BYTES,
32617
+ "body too large"
32618
+ );
32619
+ if (parsed instanceof Response) return parsed;
32620
+ const body = parsed ?? {};
32621
+ const resolved = await resolveListedPath(cwd2, bodyString(body, "path"));
32622
+ if (resolved instanceof Response) return resolved;
32623
+ if (resolved.current) {
32624
+ return textError("cannot stop the server serving this worktree", 409);
32625
+ }
32626
+ await stopWorktreeServer(resolved.path);
32627
+ return actionJson({});
32628
+ }
32629
+ async function handleOpenPost2(req, cwd2) {
32630
+ const parsed = await parseBoundedJsonBody(
32631
+ req,
32632
+ BODY_MAX_BYTES,
32633
+ "body too large"
32634
+ );
32635
+ if (parsed instanceof Response) return parsed;
32636
+ const body = parsed ?? {};
32637
+ const resolved = await resolveListedPath(cwd2, bodyString(body, "path"));
32638
+ if (resolved instanceof Response) return resolved;
32639
+ const result = await openWorktreeServer(resolved.path);
32640
+ if (result.status === "missing") return textError("worktree is gone", 410);
32641
+ if (result.status === "timeout") {
32642
+ return textError("code-viewer did not come up in time", 504);
32643
+ }
32644
+ if (result.status === "error") {
32645
+ console.error("[code-viewer] worktree open failed", result.error);
32646
+ return textError(formatErrorDetail(result.error), 500);
32647
+ }
32648
+ return actionJson({ url: result.url, started: result.started });
32649
+ }
32650
+ function handleWorktreeRoute(req, url, cwd2, generation2, sideEffectAllowed) {
32651
+ return dispatchRoutes(
32652
+ req,
32653
+ url,
32654
+ {
32655
+ "/_worktree/list": {
32656
+ methods: ["GET"],
32657
+ sideEffect: false,
32658
+ handler: () => handleListGet(cwd2, generation2)
32659
+ },
32660
+ "/_worktree/diff": {
32661
+ methods: ["GET"],
32662
+ sideEffect: false,
32663
+ handler: () => handleDiffGet(url, cwd2, generation2)
32664
+ },
32665
+ "/_worktree/add": {
32666
+ methods: ["POST"],
32667
+ sideEffect: true,
32668
+ handler: () => handleAddPost(req, cwd2)
32669
+ },
32670
+ "/_worktree/remove": {
32671
+ methods: ["POST"],
32672
+ sideEffect: true,
32673
+ handler: () => handleRemovePost(req, cwd2)
32674
+ },
32675
+ "/_worktree/open": {
32676
+ methods: ["POST"],
32677
+ sideEffect: true,
32678
+ handler: () => handleOpenPost2(req, cwd2)
32679
+ },
32680
+ "/_worktree/stop": {
32681
+ methods: ["POST"],
32682
+ sideEffect: true,
32683
+ handler: () => handleStopPost(req, cwd2)
32684
+ }
32685
+ },
32686
+ sideEffectAllowed,
32687
+ (res) => res,
32688
+ (err) => handleError("worktree", "handle worktree request", err)
32689
+ );
32690
+ }
32691
+ var BODY_MAX_BYTES, DIFF_MAX_HUNKS, DIFF_MAX_LINES;
32692
+ var init_handle5 = __esm({
32693
+ "web-src/server/worktree/handle.ts"() {
32694
+ init_error_detail();
32695
+ init_worktree();
32696
+ init_handle_shared();
32697
+ init_file_cli();
32698
+ init_git();
32699
+ init_search_service();
32700
+ init_list();
32701
+ init_open();
32702
+ BODY_MAX_BYTES = 8 * 1024;
32703
+ DIFF_MAX_HUNKS = 200;
32704
+ DIFF_MAX_LINES = 2e4;
32705
+ }
32706
+ });
32707
+
31479
32708
  // web-src/server/state-route.ts
31480
32709
  var state_route_exports = {};
31481
32710
  __export(state_route_exports, {
@@ -31595,14 +32824,14 @@ var init_state_route = __esm({
31595
32824
  // web-src/server/preview.ts
31596
32825
  var preview_exports = {};
31597
32826
  import {
31598
- existsSync as existsSync9,
32827
+ existsSync as existsSync10,
31599
32828
  mkdirSync as mkdirSync5,
31600
32829
  readFileSync as readFileSync8,
31601
- realpathSync as realpathSync8,
32830
+ realpathSync as realpathSync9,
31602
32831
  statSync as statSync8,
31603
32832
  watch
31604
32833
  } from "node:fs";
31605
- import { basename as basename5, dirname as dirname7, extname as extname2, join as join25, relative as relative8 } from "node:path";
32834
+ import { basename as basename6, dirname as dirname7, extname as extname2, join as join27, relative as relative9 } from "node:path";
31606
32835
  function parseCli() {
31607
32836
  const rest = [];
31608
32837
  for (let i = 2; i < process.argv.length; i++) {
@@ -31657,7 +32886,7 @@ Examples:
31657
32886
  process.exit(1);
31658
32887
  }
31659
32888
  try {
31660
- cwd = realpathSync8(next);
32889
+ cwd = realpathSync9(next);
31661
32890
  cwdWasExplicit = true;
31662
32891
  } catch {
31663
32892
  console.error("--cwd must point to an existing directory");
@@ -31726,7 +32955,7 @@ Examples:
31726
32955
  }
31727
32956
  function warnIfLegacyConfigPresent() {
31728
32957
  try {
31729
- if (existsSync9(join25(cwd, ".code-viewer.json"))) {
32958
+ if (existsSync10(join27(cwd, ".code-viewer.json"))) {
31730
32959
  console.warn(
31731
32960
  "[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed."
31732
32961
  );
@@ -31833,8 +33062,8 @@ function staticFile(pathname) {
31833
33062
  }
31834
33063
  const spec = map[pathname];
31835
33064
  if (!spec) return null;
31836
- const full = join25(WEB_ROOT, spec[0]);
31837
- if (!existsSync9(full)) return text("not found", 404);
33065
+ const full = join27(WEB_ROOT, spec[0]);
33066
+ if (!existsSync10(full)) return text("not found", 404);
31838
33067
  return new Response(readFileSync8(full), {
31839
33068
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
31840
33069
  });
@@ -31928,7 +33157,7 @@ async function computePayload(extras, range, pathFilter = "", responseGeneration
31928
33157
  files: [],
31929
33158
  totals: { files: 0, additions: 0, deletions: 0 },
31930
33159
  range: "worktree .. worktree",
31931
- project: basename5(cwd),
33160
+ project: basename6(cwd),
31932
33161
  branch: await currentBranchMetadata(),
31933
33162
  generation: responseGeneration
31934
33163
  };
@@ -31937,8 +33166,11 @@ async function computePayload(extras, range, pathFilter = "", responseGeneration
31937
33166
  const fullArgs = pathFilter ? [...extras, ...args, "--", pathFilter] : [...extras, ...args];
31938
33167
  const metaResult = await fileMetaResultAsync(fullArgs, cwd, false);
31939
33168
  const files = metaResult.files;
33169
+ let metaError = metaResult.error;
31940
33170
  if (!metaResult.error && includeUntracked(range, refs)) {
31941
- files.push(...await untrackedMetaAsync(cwd));
33171
+ const untracked = await untrackedMetaAsync(cwd);
33172
+ files.push(...untracked.files);
33173
+ metaError = untracked.error;
31942
33174
  }
31943
33175
  const filteredFiles = pathFilter ? files.filter(
31944
33176
  (file) => file.path === pathFilter || file.old_path === pathFilter
@@ -31971,10 +33203,10 @@ async function computePayload(extras, range, pathFilter = "", responseGeneration
31971
33203
  files: meta,
31972
33204
  totals,
31973
33205
  range: label || "HEAD",
31974
- project: basename5(cwd),
33206
+ project: basename6(cwd),
31975
33207
  branch: await currentBranchMetadata(),
31976
33208
  generation: responseGeneration,
31977
- ...metaResult.error ? { error: metaResult.error } : {}
33209
+ ...metaError ? { error: metaError } : {}
31978
33210
  };
31979
33211
  }
31980
33212
  async function handleDiffJson(url) {
@@ -32092,12 +33324,12 @@ function safeWorktreePath2(path) {
32092
33324
  return safeWorktreePath(currentSearchEnv(), path);
32093
33325
  }
32094
33326
  function worktreePath(path) {
32095
- return join25(cwd, path);
33327
+ return join27(cwd, path);
32096
33328
  }
32097
33329
  function safeOpenWorktreePath(path) {
32098
33330
  if (path === "") {
32099
33331
  try {
32100
- const realCwd = realpathSync8(cwd);
33332
+ const realCwd = realpathSync9(cwd);
32101
33333
  if (isGitInternalPath(realCwd)) return null;
32102
33334
  return realCwd;
32103
33335
  } catch {
@@ -32117,11 +33349,11 @@ function worktreeFileMetadata(path, knownSize) {
32117
33349
  const full = safeWorktreePath2(path);
32118
33350
  if (!full) return {};
32119
33351
  try {
32120
- const stat3 = statSync8(full);
33352
+ const stat4 = statSync8(full);
32121
33353
  return {
32122
- size: knownSize ?? stat3.size,
32123
- created_at: isoDate(stat3.birthtimeMs),
32124
- updated_at: isoDate(stat3.mtimeMs)
33354
+ size: knownSize ?? stat4.size,
33355
+ created_at: isoDate(stat4.birthtimeMs),
33356
+ updated_at: isoDate(stat4.mtimeMs)
32125
33357
  };
32126
33358
  } catch {
32127
33359
  return {};
@@ -32141,10 +33373,10 @@ async function directoryMetadata(target, path) {
32141
33373
  const full = path === "" ? safeOpenWorktreePath("") : safeWorktreePath2(path);
32142
33374
  if (!full) return {};
32143
33375
  try {
32144
- const stat3 = statSync8(full);
33376
+ const stat4 = statSync8(full);
32145
33377
  return {
32146
- created_at: isoDate(stat3.birthtimeMs),
32147
- updated_at: isoDate(stat3.mtimeMs)
33378
+ created_at: isoDate(stat4.birthtimeMs),
33379
+ updated_at: isoDate(stat4.mtimeMs)
32148
33380
  };
32149
33381
  } catch {
32150
33382
  return {};
@@ -32256,7 +33488,7 @@ async function handleTree(url) {
32256
33488
  return json2({
32257
33489
  ref: target,
32258
33490
  path,
32259
- project: basename5(cwd),
33491
+ project: basename6(cwd),
32260
33492
  branch: await currentBranchMetadata(),
32261
33493
  entries: recursive ? entries.map(withStatus) : [
32262
33494
  ...await Promise.all(
@@ -32272,9 +33504,10 @@ async function handleTree(url) {
32272
33504
  }
32273
33505
  async function handleSettings() {
32274
33506
  return json2({
32275
- project: basename5(cwd),
33507
+ project: basename6(cwd),
32276
33508
  branch: await currentBranchMetadata(),
32277
33509
  repo_web_url: cwdHasGitRepository ? await remoteWebUrlAsync(cwd) : null,
33510
+ server: { pid: process.pid, root: cwd },
32278
33511
  scope: {
32279
33512
  omit_dirs_effective: scopeOmitDirNames,
32280
33513
  omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
@@ -32429,7 +33662,7 @@ async function handleLog(url) {
32429
33662
  }
32430
33663
  function blamePathKey(p) {
32431
33664
  try {
32432
- const st = statSync8(join25(cwd, p));
33665
+ const st = statSync8(join27(cwd, p));
32433
33666
  return `${st.mtimeMs}:${st.size}`;
32434
33667
  } catch {
32435
33668
  return "missing";
@@ -32585,8 +33818,8 @@ async function handleFileDiff(url) {
32585
33818
  }
32586
33819
  function worktreeLineIndexSignature(full) {
32587
33820
  try {
32588
- const stat3 = statSync8(full);
32589
- return `size:${stat3.size}|mtime:${stat3.mtimeMs}|ctime:${stat3.ctimeMs}|ino:${stat3.ino || 0}`;
33821
+ const stat4 = statSync8(full);
33822
+ return `size:${stat4.size}|mtime:${stat4.mtimeMs}|ctime:${stat4.ctimeMs}|ino:${stat4.ino || 0}`;
32590
33823
  } catch {
32591
33824
  return null;
32592
33825
  }
@@ -32600,11 +33833,11 @@ async function getWorktreeLineIndex(full) {
32600
33833
  lineIndexCache.set(full, cached);
32601
33834
  return cached.index;
32602
33835
  }
32603
- const stat3 = statSync8(full);
32604
- if (stat3.size > LINE_INDEX_MAX_FILE_BYTES) return null;
33836
+ const stat4 = statSync8(full);
33837
+ if (stat4.size > LINE_INDEX_MAX_FILE_BYTES) return null;
32605
33838
  const index = await buildLineOffsetIndexFromStream(
32606
33839
  fileReadableStream(full),
32607
- stat3.size
33840
+ stat4.size
32608
33841
  );
32609
33842
  lineIndexCache.delete(full);
32610
33843
  lineIndexCache.set(full, { signature, index });
@@ -32960,10 +34193,10 @@ async function handleUploadFiles(req) {
32960
34193
  if (file.size > MAX_UPLOAD_FILE_BYTES) return text("file too large", 413);
32961
34194
  total += file.size;
32962
34195
  if (total > MAX_UPLOAD_TOTAL_BYTES) return text("upload too large", 413);
32963
- const target = join25(realDir, safeName);
32964
- if (relative8(realDir, dirname7(target)) !== "")
34196
+ const target = join27(realDir, safeName);
34197
+ if (relative9(realDir, dirname7(target)) !== "")
32965
34198
  return text("invalid filename", 400);
32966
- if (existsSync9(target)) return text("file exists", 409);
34199
+ if (existsSync10(target)) return text("file exists", 409);
32967
34200
  uploads.push({ file, name: safeName, target });
32968
34201
  }
32969
34202
  try {
@@ -33111,8 +34344,8 @@ async function handleCreateDirectory(req) {
33111
34344
  const targetPath = dir ? `${dir}/${name}` : name;
33112
34345
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
33113
34346
  return text("invalid target", 400);
33114
- const target = join25(parent, name);
33115
- if (existsSync9(target)) return text("already exists", 409);
34347
+ const target = join27(parent, name);
34348
+ if (existsSync10(target)) return text("already exists", 409);
33116
34349
  try {
33117
34350
  mkdirSync5(target, { recursive: false });
33118
34351
  } catch (error) {
@@ -33211,7 +34444,7 @@ async function handleMcp(req) {
33211
34444
  function journalSse(kind, id) {
33212
34445
  sendSse("journal", JSON.stringify({ kind, id }));
33213
34446
  }
33214
- function bodyString(body, key) {
34447
+ function bodyString2(body, key) {
33215
34448
  const value = body[key];
33216
34449
  return typeof value === "string" ? value : void 0;
33217
34450
  }
@@ -33273,9 +34506,9 @@ async function handleJournal(req) {
33273
34506
  const result = addDailyJournalEntry(
33274
34507
  state,
33275
34508
  {
33276
- date: bodyString(body, "date") || "",
33277
- title: bodyString(body, "title"),
33278
- body: bodyString(body, "body") || "",
34509
+ date: bodyString2(body, "date") || "",
34510
+ title: bodyString2(body, "title"),
34511
+ body: bodyString2(body, "body") || "",
33279
34512
  labels: body.labels,
33280
34513
  source: body.source === "ai" ? "ai" : "user"
33281
34514
  },
@@ -33288,30 +34521,30 @@ async function handleJournal(req) {
33288
34521
  return json2({ ok: true, entry, generation });
33289
34522
  }
33290
34523
  if (action === "list-github-issues") {
33291
- const label = bodyString(body, "label");
34524
+ const label = bodyString2(body, "label");
33292
34525
  const labels = bodyStringList(body, "labels");
33293
34526
  if (label) labels.push(label);
33294
34527
  const issues = await readGithubIssueListAsync({
33295
34528
  cwd,
33296
- repo: bodyString(body, "repo"),
34529
+ repo: bodyString2(body, "repo"),
33297
34530
  labels,
33298
- search: bodyString(body, "search"),
33299
- state: normalizeGithubIssueListState(bodyString(body, "state")),
34531
+ search: bodyString2(body, "search"),
34532
+ state: normalizeGithubIssueListState(bodyString2(body, "state")),
33300
34533
  limit: normalizeGithubIssueListLimit(bodyNumber(body, "limit"))
33301
34534
  });
33302
34535
  return json2({ ok: true, issues, generation });
33303
34536
  }
33304
34537
  if (action === "update-entry") {
33305
- const id = bodyString(body, "id") || "";
34538
+ const id = bodyString2(body, "id") || "";
33306
34539
  if (!id) return text("invalid id", 400);
33307
34540
  const entry = await updateDailyJournalState(cwd, (state) => {
33308
34541
  const result = updateDailyJournalEntry(
33309
34542
  state,
33310
34543
  id,
33311
34544
  {
33312
- date: bodyString(body, "date"),
33313
- title: bodyString(body, "title"),
33314
- body: bodyString(body, "body"),
34545
+ date: bodyString2(body, "date"),
34546
+ title: bodyString2(body, "title"),
34547
+ body: bodyString2(body, "body"),
33315
34548
  labels: body.labels,
33316
34549
  source: body.source === "ai" ? "ai" : void 0
33317
34550
  },
@@ -33324,7 +34557,7 @@ async function handleJournal(req) {
33324
34557
  return json2({ ok: true, entry, generation });
33325
34558
  }
33326
34559
  if (action === "delete-entry") {
33327
- const id = bodyString(body, "id") || "";
34560
+ const id = bodyString2(body, "id") || "";
33328
34561
  if (!id) return text("invalid id", 400);
33329
34562
  const removed = await updateDailyJournalState(cwd, (state) => {
33330
34563
  const result = deleteDailyJournalEntry(state, id);
@@ -33339,15 +34572,15 @@ async function handleJournal(req) {
33339
34572
  state,
33340
34573
  {
33341
34574
  issue_number: bodyNumber(body, "issue_number") || 0,
33342
- repo: bodyString(body, "repo"),
33343
- title: bodyString(body, "title"),
33344
- url: bodyString(body, "url"),
33345
- memo_label: bodyString(body, "memo_label"),
34575
+ repo: bodyString2(body, "repo"),
34576
+ title: bodyString2(body, "title"),
34577
+ url: bodyString2(body, "url"),
34578
+ memo_label: bodyString2(body, "memo_label"),
33346
34579
  status: bodyTaskStatus(body, "status"),
33347
34580
  priority: bodyTaskPriority(body, "priority"),
33348
34581
  labels: body.labels,
33349
- before_id: bodyString(body, "before_id"),
33350
- after_id: bodyString(body, "after_id"),
34582
+ before_id: bodyString2(body, "before_id"),
34583
+ after_id: bodyString2(body, "after_id"),
33351
34584
  position: bodyNumber(body, "position")
33352
34585
  },
33353
34586
  now
@@ -33369,16 +34602,16 @@ async function handleJournal(req) {
33369
34602
  const result = addJournalTask(
33370
34603
  state,
33371
34604
  {
33372
- title: bodyString(body, "title") || "",
33373
- body: bodyString(body, "body"),
34605
+ title: bodyString2(body, "title") || "",
34606
+ body: bodyString2(body, "body"),
33374
34607
  status: bodyTaskStatus(body, "status"),
33375
34608
  priority: bodyTaskPriority(body, "priority"),
33376
34609
  labels: body.labels,
33377
- due_date: bodyString(body, "due_date"),
33378
- source_date: bodyString(body, "source_date"),
33379
- journal_entry_id: bodyString(body, "journal_entry_id"),
33380
- before_id: bodyString(body, "before_id"),
33381
- after_id: bodyString(body, "after_id"),
34610
+ due_date: bodyString2(body, "due_date"),
34611
+ source_date: bodyString2(body, "source_date"),
34612
+ journal_entry_id: bodyString2(body, "journal_entry_id"),
34613
+ before_id: bodyString2(body, "before_id"),
34614
+ after_id: bodyString2(body, "after_id"),
33382
34615
  position: bodyNumber(body, "position")
33383
34616
  },
33384
34617
  now
@@ -33390,21 +34623,21 @@ async function handleJournal(req) {
33390
34623
  return json2({ ok: true, task, generation });
33391
34624
  }
33392
34625
  if (action === "update-task") {
33393
- const id = bodyString(body, "id") || "";
34626
+ const id = bodyString2(body, "id") || "";
33394
34627
  if (!id) return text("invalid id", 400);
33395
34628
  const task = await updateJournalTaskState(cwd, (state) => {
33396
34629
  const result = updateJournalTask(
33397
34630
  state,
33398
34631
  id,
33399
34632
  {
33400
- title: bodyString(body, "title"),
33401
- body: bodyString(body, "body"),
34633
+ title: bodyString2(body, "title"),
34634
+ body: bodyString2(body, "body"),
33402
34635
  status: bodyTaskStatus(body, "status"),
33403
34636
  priority: bodyTaskPriority(body, "priority"),
33404
34637
  labels: body.labels,
33405
- due_date: body.due_date === null ? null : bodyString(body, "due_date"),
33406
- source_date: body.source_date === null ? null : bodyString(body, "source_date"),
33407
- journal_entry_id: body.journal_entry_id === null ? null : bodyString(body, "journal_entry_id")
34638
+ due_date: body.due_date === null ? null : bodyString2(body, "due_date"),
34639
+ source_date: body.source_date === null ? null : bodyString2(body, "source_date"),
34640
+ journal_entry_id: body.journal_entry_id === null ? null : bodyString2(body, "journal_entry_id")
33408
34641
  },
33409
34642
  now
33410
34643
  );
@@ -33415,7 +34648,7 @@ async function handleJournal(req) {
33415
34648
  return json2({ ok: true, task, generation });
33416
34649
  }
33417
34650
  if (action === "move-task") {
33418
- const id = bodyString(body, "id") || "";
34651
+ const id = bodyString2(body, "id") || "";
33419
34652
  if (!id) return text("invalid id", 400);
33420
34653
  const task = await updateJournalTaskState(cwd, (state) => {
33421
34654
  const result = moveJournalTask(
@@ -33423,8 +34656,8 @@ async function handleJournal(req) {
33423
34656
  id,
33424
34657
  {
33425
34658
  status: bodyTaskStatus(body, "status"),
33426
- before_id: bodyString(body, "before_id"),
33427
- after_id: bodyString(body, "after_id"),
34659
+ before_id: bodyString2(body, "before_id"),
34660
+ after_id: bodyString2(body, "after_id"),
33428
34661
  position: bodyNumber(body, "position")
33429
34662
  },
33430
34663
  now
@@ -33436,14 +34669,14 @@ async function handleJournal(req) {
33436
34669
  return json2({ ok: true, task, generation });
33437
34670
  }
33438
34671
  if (action === "claim-task") {
33439
- const id = bodyString(body, "id") || "";
34672
+ const id = bodyString2(body, "id") || "";
33440
34673
  if (!id) return text("invalid id", 400);
33441
34674
  const task = await updateJournalTaskState(cwd, (state) => {
33442
34675
  const result = claimJournalTask(
33443
34676
  state,
33444
34677
  id,
33445
34678
  {
33446
- by: bodyString(body, "by"),
34679
+ by: bodyString2(body, "by"),
33447
34680
  lease_minutes: bodyNumber(body, "lease_minutes"),
33448
34681
  wip_limit: bodyNumber(body, "wip_limit")
33449
34682
  },
@@ -33456,15 +34689,15 @@ async function handleJournal(req) {
33456
34689
  return json2({ ok: true, task, generation });
33457
34690
  }
33458
34691
  if (action === "complete-task") {
33459
- const id = bodyString(body, "id") || "";
34692
+ const id = bodyString2(body, "id") || "";
33460
34693
  if (!id) return text("invalid id", 400);
33461
34694
  const task = await updateJournalTaskState(cwd, (state) => {
33462
34695
  const result = completeJournalTask(
33463
34696
  state,
33464
34697
  id,
33465
34698
  {
33466
- by: bodyString(body, "by"),
33467
- note: bodyString(body, "note"),
34699
+ by: bodyString2(body, "by"),
34700
+ note: bodyString2(body, "note"),
33468
34701
  source: body.source === "user" ? "user" : "ai"
33469
34702
  },
33470
34703
  now
@@ -33476,7 +34709,7 @@ async function handleJournal(req) {
33476
34709
  return json2({ ok: true, task, generation });
33477
34710
  }
33478
34711
  if (action === "delete-task") {
33479
- const id = bodyString(body, "id") || "";
34712
+ const id = bodyString2(body, "id") || "";
33480
34713
  if (!id) return text("invalid id", 400);
33481
34714
  const removed = await updateJournalTaskState(cwd, (state) => {
33482
34715
  const result = deleteJournalTask(state, id);
@@ -33655,12 +34888,24 @@ function closeSseClients() {
33655
34888
  }
33656
34889
  }
33657
34890
  }
34891
+ function removeOwnServerRegistry() {
34892
+ registryCleanupAttempted = true;
34893
+ removeServerRegistry(cwd, process.pid);
34894
+ }
33658
34895
  async function shutdown(exitCode = 0) {
33659
34896
  if (shuttingDown) {
33660
34897
  process.exit(1);
33661
34898
  }
33662
34899
  shuttingDown = true;
33663
- removeServerRegistry(cwd, process.pid);
34900
+ try {
34901
+ removeOwnServerRegistry();
34902
+ } catch (error) {
34903
+ exitCode = 1;
34904
+ console.error(
34905
+ `code-viewer registry cleanup failed:
34906
+ ${formatErrorDetail(error)}`
34907
+ );
34908
+ }
33664
34909
  closeSseClients();
33665
34910
  try {
33666
34911
  const [{ closeShellStreams: closeShellStreams2 }, { closeAllShellSessions: closeAllShellSessions2 }] = await Promise.all([shellHandleModule, Promise.resolve().then(() => (init_session(), session_exports))]);
@@ -33733,7 +34978,7 @@ function restartWorktreeWatch() {
33733
34978
  }
33734
34979
  worktreeWatch = startScopedWorktreeWatch();
33735
34980
  }
33736
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT, PREVIEW_LINES_DEFAULT, WATCHED_ASSET_FILES, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE, LINE_INDEX_MIN_START, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES, SAFE_UPLOAD_EXTENSIONS, generation, cwd, cliArgs, listenPort, openAfterStart, commandOverrides, cwdWasExplicit, cwdHasGitRepository, scopeOmitDirNames, scopeOmitDirCliOverride, scopeExcludeNames, scopeWatchLimit, uploadEnabled, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX, metaCache, diffMetaRequestSequence, latestDiffMetaRequest, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes, safePath, MCP_MAX_BODY_BYTES, MCP_INSTRUCTIONS, JournalRequestError, isCodeViewerInternalPath, watchLimitReached, databaseHandleModule, tmuxHandleModule, shellHandleModule, agentHandleModule, server, worktreeWatch, shuttingDown;
34981
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT, PREVIEW_LINES_DEFAULT, WATCHED_ASSET_FILES, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE, LINE_INDEX_MIN_START, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES, SAFE_UPLOAD_EXTENSIONS, generation, cwd, cliArgs, listenPort, openAfterStart, commandOverrides, cwdWasExplicit, cwdHasGitRepository, scopeOmitDirNames, scopeOmitDirCliOverride, scopeExcludeNames, scopeWatchLimit, uploadEnabled, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX, metaCache, diffMetaRequestSequence, latestDiffMetaRequest, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes, safePath, MCP_MAX_BODY_BYTES, MCP_INSTRUCTIONS, JournalRequestError, isCodeViewerInternalPath, watchLimitReached, databaseHandleModule, tmuxHandleModule, shellHandleModule, agentHandleModule, worktreeHandleModule, server, worktreeWatch, shuttingDown, registryCleanupAttempted;
33737
34982
  var init_preview = __esm({
33738
34983
  async "web-src/server/preview.ts"() {
33739
34984
  init_directory_name();
@@ -33763,8 +35008,8 @@ var init_preview = __esm({
33763
35008
  init_state_store();
33764
35009
  init_watch_supervisor();
33765
35010
  init_worktree_watcher();
33766
- WEB_ROOT = join25(ROOT, "web");
33767
- VERSION = JSON.parse(readFileSync8(join25(ROOT, "package.json"), "utf8")).version;
35011
+ WEB_ROOT = join27(ROOT, "web");
35012
+ VERSION = JSON.parse(readFileSync8(join27(ROOT, "package.json"), "utf8")).version;
33768
35013
  DEFAULT_ARGS = ["HEAD"];
33769
35014
  PREVIEW_HUNKS_DEFAULT = 3;
33770
35015
  PREVIEW_LINES_DEFAULT = 1200;
@@ -33859,6 +35104,7 @@ var init_preview = __esm({
33859
35104
  tmuxHandleModule = Promise.resolve().then(() => (init_handle2(), handle_exports2));
33860
35105
  shellHandleModule = Promise.resolve().then(() => (init_handle3(), handle_exports3));
33861
35106
  agentHandleModule = Promise.resolve().then(() => (init_handle4(), handle_exports4));
35107
+ worktreeHandleModule = Promise.resolve().then(() => (init_handle5(), handle_exports5));
33862
35108
  server = await startServer({
33863
35109
  hostname: "127.0.0.1",
33864
35110
  port: listenPort,
@@ -33873,7 +35119,8 @@ var init_preview = __esm({
33873
35119
  return handleDoctor({
33874
35120
  cwd,
33875
35121
  scopeOmitDirNames,
33876
- listenPort
35122
+ listenPort,
35123
+ signal: req.signal
33877
35124
  });
33878
35125
  if (url.pathname === "/_tree") return await handleTree(url);
33879
35126
  if (url.pathname === "/_files") return await handleFiles2(url);
@@ -33912,6 +35159,17 @@ var init_preview = __esm({
33912
35159
  );
33913
35160
  if (tmuxResponse) return tmuxResponse;
33914
35161
  }
35162
+ if (url.pathname.startsWith("/_worktree/")) {
35163
+ const { handleWorktreeRoute: handleWorktreeRoute2 } = await worktreeHandleModule;
35164
+ const worktreeResponse = await handleWorktreeRoute2(
35165
+ req,
35166
+ url,
35167
+ cwd,
35168
+ generation,
35169
+ sideEffectRequestAllowed2
35170
+ );
35171
+ if (worktreeResponse) return worktreeResponse;
35172
+ }
33915
35173
  if (url.pathname.startsWith("/_shell/")) {
33916
35174
  const { handleShellRoute: handleShellRoute2 } = await shellHandleModule;
33917
35175
  const shellResponse = await handleShellRoute2(
@@ -34012,6 +35270,7 @@ data: ${watchLimitReached}
34012
35270
  });
34013
35271
  worktreeWatch = null;
34014
35272
  shuttingDown = false;
35273
+ registryCleanupAttempted = false;
34015
35274
  process.on("uncaughtException", (error) => {
34016
35275
  console.error(
34017
35276
  "[code-viewer] uncaught exception (server kept running):",
@@ -34025,7 +35284,17 @@ data: ${watchLimitReached}
34025
35284
  );
34026
35285
  });
34027
35286
  process.on("exit", () => {
34028
- removeServerRegistry(cwd, process.pid);
35287
+ if (!registryCleanupAttempted) {
35288
+ try {
35289
+ removeOwnServerRegistry();
35290
+ } catch (error) {
35291
+ process.exitCode = 1;
35292
+ console.error(
35293
+ `code-viewer registry cleanup failed:
35294
+ ${formatErrorDetail(error)}`
35295
+ );
35296
+ }
35297
+ }
34029
35298
  closeSseClients();
34030
35299
  worktreeWatch?.close();
34031
35300
  });