@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.
- package/README.md +14 -0
- package/dist/code-viewer.js +1515 -246
- package/package.json +1 -1
- package/web/app.js +2328 -349
- package/web/index.html +5 -0
- package/web/style.css +335 -39
package/dist/code-viewer.js
CHANGED
|
@@ -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
|
-
|
|
1690
|
-
|
|
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
|
-
[
|
|
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)
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2376
|
-
if (!
|
|
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)
|
|
2500
|
-
|
|
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
|
|
2553
|
-
symlink_target_type =
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
-
|
|
3456
|
+
let raw;
|
|
3096
3457
|
try {
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
4740
|
-
if (!
|
|
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
|
|
8911
|
-
const tier = pathMatchTier(q, lowerPath,
|
|
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
|
|
9064
|
-
if (!regex.test(path) && (query.includes("/") || !regex.test(
|
|
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
|
|
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 (
|
|
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
|
|
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 (!
|
|
10158
|
+
if (!existsSync4(skillsRoot)) return [];
|
|
9786
10159
|
return readdirSync2(skillsRoot, { withFileTypes: true }).filter(
|
|
9787
|
-
(entry) => entry.isDirectory() &&
|
|
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 =
|
|
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
|
|
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
|
|
16258
|
-
if (!
|
|
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 (!
|
|
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
|
|
18877
|
-
const result = await
|
|
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:${
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
24811
|
-
dbDetail = `${dbPath} (${
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
27121
|
+
import { join as join19 } from "node:path";
|
|
26147
27122
|
function dailyJournalFilePath(root) {
|
|
26148
|
-
return
|
|
27123
|
+
return join19(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
|
|
26149
27124
|
}
|
|
26150
27125
|
function journalTasksFilePath(root) {
|
|
26151
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
26776
|
-
realFull =
|
|
27750
|
+
realCwd = realpathSync7(env.cwd);
|
|
27751
|
+
realFull = realpathSync7(full);
|
|
26777
27752
|
} catch {
|
|
26778
27753
|
return null;
|
|
26779
27754
|
}
|
|
26780
|
-
const rel =
|
|
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
|
|
27779
|
+
let stat4;
|
|
26805
27780
|
try {
|
|
26806
|
-
|
|
27781
|
+
stat4 = await lstat2(full);
|
|
26807
27782
|
} catch {
|
|
26808
27783
|
continue;
|
|
26809
27784
|
}
|
|
26810
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
31436
|
+
return join23(cwd2, ".code-viewer", "trash");
|
|
30462
31437
|
}
|
|
30463
31438
|
function movePathIntoTrashDirectory(path, trashRoot) {
|
|
30464
31439
|
mkdirSync4(trashRoot, { recursive: true });
|
|
30465
|
-
const name =
|
|
30466
|
-
const trashPath =
|
|
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
|
|
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,
|
|
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
|
|
30578
|
-
import { join as
|
|
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 ?
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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
|
|
32154
|
+
import { realpathSync as realpathSync8, statSync as statSync7 } from "node:fs";
|
|
31180
32155
|
import { homedir as homedir4 } from "node:os";
|
|
31181
|
-
import { basename as
|
|
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 =
|
|
31190
|
-
const
|
|
31191
|
-
if (!
|
|
31192
|
-
if (
|
|
31193
|
-
return { path: real, bytes:
|
|
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:
|
|
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
|
|
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 =
|
|
31255
|
-
const path =
|
|
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 =
|
|
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
|
|
32827
|
+
existsSync as existsSync10,
|
|
31599
32828
|
mkdirSync as mkdirSync5,
|
|
31600
32829
|
readFileSync as readFileSync8,
|
|
31601
|
-
realpathSync as
|
|
32830
|
+
realpathSync as realpathSync9,
|
|
31602
32831
|
statSync as statSync8,
|
|
31603
32832
|
watch
|
|
31604
32833
|
} from "node:fs";
|
|
31605
|
-
import { basename as
|
|
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 =
|
|
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 (
|
|
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 =
|
|
31837
|
-
if (!
|
|
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:
|
|
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
|
-
|
|
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:
|
|
33206
|
+
project: basename6(cwd),
|
|
31975
33207
|
branch: await currentBranchMetadata(),
|
|
31976
33208
|
generation: responseGeneration,
|
|
31977
|
-
...
|
|
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
|
|
33327
|
+
return join27(cwd, path);
|
|
32096
33328
|
}
|
|
32097
33329
|
function safeOpenWorktreePath(path) {
|
|
32098
33330
|
if (path === "") {
|
|
32099
33331
|
try {
|
|
32100
|
-
const realCwd =
|
|
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
|
|
33352
|
+
const stat4 = statSync8(full);
|
|
32121
33353
|
return {
|
|
32122
|
-
size: knownSize ??
|
|
32123
|
-
created_at: isoDate(
|
|
32124
|
-
updated_at: isoDate(
|
|
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
|
|
33376
|
+
const stat4 = statSync8(full);
|
|
32145
33377
|
return {
|
|
32146
|
-
created_at: isoDate(
|
|
32147
|
-
updated_at: isoDate(
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
32589
|
-
return `size:${
|
|
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
|
|
32604
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
32964
|
-
if (
|
|
34196
|
+
const target = join27(realDir, safeName);
|
|
34197
|
+
if (relative9(realDir, dirname7(target)) !== "")
|
|
32965
34198
|
return text("invalid filename", 400);
|
|
32966
|
-
if (
|
|
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 =
|
|
33115
|
-
if (
|
|
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
|
|
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:
|
|
33277
|
-
title:
|
|
33278
|
-
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 =
|
|
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:
|
|
34529
|
+
repo: bodyString2(body, "repo"),
|
|
33297
34530
|
labels,
|
|
33298
|
-
search:
|
|
33299
|
-
state: normalizeGithubIssueListState(
|
|
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 =
|
|
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:
|
|
33313
|
-
title:
|
|
33314
|
-
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 =
|
|
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:
|
|
33343
|
-
title:
|
|
33344
|
-
url:
|
|
33345
|
-
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:
|
|
33350
|
-
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:
|
|
33373
|
-
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:
|
|
33378
|
-
source_date:
|
|
33379
|
-
journal_entry_id:
|
|
33380
|
-
before_id:
|
|
33381
|
-
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 =
|
|
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:
|
|
33401
|
-
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 :
|
|
33406
|
-
source_date: body.source_date === null ? null :
|
|
33407
|
-
journal_entry_id: body.journal_entry_id === null ? null :
|
|
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 =
|
|
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:
|
|
33427
|
-
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
33467
|
-
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 =
|
|
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
|
-
|
|
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 =
|
|
33767
|
-
VERSION = JSON.parse(readFileSync8(
|
|
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
|
-
|
|
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
|
});
|